From 4e4d1cb1be7a0ff6bef500d8c841aa3b62969b07 Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 10:49:09 -0400 Subject: [PATCH 01/17] refactor cdoe in prep for client support --- .DS_Store | Bin 0 -> 6148 bytes lib/model_context_protocol.rb | 38 ++++++++++-------- lib/model_context_protocol/server.rb | 4 +- .../{ => server}/transports/stdio.rb | 3 +- .../{ => shared}/configuration.rb | 0 .../{ => shared}/content.rb | 0 .../{ => shared}/instrumentation.rb | 0 .../{ => shared}/methods.rb | 0 .../{ => shared}/prompt.rb | 4 ++ .../{ => shared}/prompt/argument.rb | 0 .../{ => shared}/prompt/message.rb | 0 .../{ => shared}/prompt/result.rb | 0 .../{ => shared}/resource.rb | 0 .../{ => shared}/resource_template.rb | 0 .../{ => shared}/string_utils.rb | 0 .../{ => shared}/tool.rb | 2 + .../{ => shared}/tool/annotations.rb | 0 .../{ => shared}/tool/input_schema.rb | 0 .../{ => shared}/tool/response.rb | 0 .../{ => shared}/transport.rb | 0 .../{ => shared}/version.rb | 2 +- model_context_protocol-1.0.0.gem | Bin 0 -> 10752 bytes model_context_protocol.gemspec | 6 +-- 23 files changed, 35 insertions(+), 24 deletions(-) create mode 100644 .DS_Store rename lib/model_context_protocol/{ => server}/transports/stdio.rb (91%) rename lib/model_context_protocol/{ => shared}/configuration.rb (100%) rename lib/model_context_protocol/{ => shared}/content.rb (100%) rename lib/model_context_protocol/{ => shared}/instrumentation.rb (100%) rename lib/model_context_protocol/{ => shared}/methods.rb (100%) rename lib/model_context_protocol/{ => shared}/prompt.rb (95%) rename lib/model_context_protocol/{ => shared}/prompt/argument.rb (100%) rename lib/model_context_protocol/{ => shared}/prompt/message.rb (100%) rename lib/model_context_protocol/{ => shared}/prompt/result.rb (100%) rename lib/model_context_protocol/{ => shared}/resource.rb (100%) rename lib/model_context_protocol/{ => shared}/resource_template.rb (100%) rename lib/model_context_protocol/{ => shared}/string_utils.rb (100%) rename lib/model_context_protocol/{ => shared}/tool.rb (98%) rename lib/model_context_protocol/{ => shared}/tool/annotations.rb (100%) rename lib/model_context_protocol/{ => shared}/tool/input_schema.rb (100%) rename lib/model_context_protocol/{ => shared}/tool/response.rb (100%) rename lib/model_context_protocol/{ => shared}/transport.rb (100%) rename lib/model_context_protocol/{ => shared}/version.rb (69%) create mode 100644 model_context_protocol-1.0.0.gem diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 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 X!2e5X@$W Date: Wed, 28 May 2025 10:49:47 -0400 Subject: [PATCH 02/17] remove gem --- model_context_protocol-1.0.0.gem | Bin 10752 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 model_context_protocol-1.0.0.gem diff --git a/model_context_protocol-1.0.0.gem b/model_context_protocol-1.0.0.gem deleted file mode 100644 index 472757d2cad1c0fb02d5a131f941a718713068db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10752 zcmeHtRZ!hewl41OE+M$PySuw?oM6E{5FkK+1b5k32<{RrI0W0c+r}Y;Kycl^^PhVk z=F~jgTjx~G+*5UDy{zu)THUL^)&0GE%iX~jWDoKMaXAIU{j-ecFB1?Dfcu;O%l^^x z3J3|q@e1KQH{l`2V*4 zzwh?<%l$+6|I{gM0vg;ODq&{?#53b<&v_oachlCO+orFtEk|DyeMJALidL-ZEH4p} ze6X@Sk{1pVw+q|xJjxe18z0Hdbm^YCEpF`XdJ2^aKiUt{vbt}xN^kp)KvNx1RQ+=f zK8t_MG=0fF#-$>z4T0!jN#Wb4g&<1KcD62fT08?u=z))P27VI)HF274s_5|UWUyt5 zeOjZC&{8bh#8?o9+}5q^k5PKGS2eNcwbv@ItH0pS86$bn??D+3u+xs8Pun#b;^I#1 zgHyG&E>GC`att=>v$1F|8400%Xgn5jq&HGG&G3yfw=a+Nk%9~2T9l5_2uXzMY7`ow z&M%~=E=Znzylxa@e?l&SNUeD{u?lkP3IG5>GNZ2moE+>j>>FwBJZOYD{M2 z&P;}>>{FKCB}hH9KYnW0x{Ne0D1LP*)MQp%$0(`*wjubsSn9NZIdP(alMSQoG;HEZ zJ8gjsED!#OYU*|WevRjLDw@8DHAm|S--$vmeSV#KXCf!JqgiAB-yXIoMJ`whch$0#0sMFc5$8qXIkg}UHKQ?`X=r89Ff-_aK z#WC@##M%tUzR2Y6ad`|=`E9yYvtytAhgdtJICFU0wqXUfHfP_2=K3fXVhgZ%sd1t{ zK?Z)peG)YJ35papiW&^AP$4YEG&F@cxUGp*8l5|w*mI&*$OYjo=cG~z)?`&&-GTWy zf@78?GVOC&9l|4u>aelh-QrwNYmS{CYDyDe;s_p#9Uv-Mr}731B* z$Fil))eZfT-?O44cZqwuESTONbF)1$$mL=iVYQ}F){e!CxevpHfVNU%F z-?zj5pZb~q8+rXN?*EGc|KIUHuK+(E&p+}%zYx#A`TxIB&i``A-}(QSAi_(z zr3DYNGNbC7#W$^;=DLB6yyTFjw1?r=EDMRwB(PQQa!6y(O9k`0U)w$JdVva{dpR6d z0@$7^(n8yx)~9Jxwo17#__dqq;Ps?FvwmWO{=7UJkxT|N@AG{G%JikDQr`w2D1(&J z=qQpoa^_oGqhZ?;8WrFfmLCJS=`Bou;*?-Kg^t%rlVCAVu!=0Mjd&~f9#}}t%elZHXHdyTHn$s>S<%x*OvGX3H<{VF98wnTM zN!HGy1U%FsK32adT&j59O%;hS_tTzkXJ9`Edie>5R@0ZYeE8u6(sVn)=>cc*c)O8a zQcmP>ZAbN)+!=vFX8F!gRq4_+aw2#lOlT4M2u*#nFxB_$~XCxAcZkkBX4b@Q1*#he7?j;0mdg)0_J^j z?^NV@-Q>jkZ9wzbx24?X`_tz#fzIe&bn;pC)Mn~&eY6f77lAjFlNE!_uSuGTrn4BW z$v^HN9)>i<>UXogRPf-ZRc)RKkR-An(8*>p&RF-yG1=oyD$j7suQ5iM5{#&3Te_+Z zxMY8jC4#B0i*7_{3|6Rno4}lynfXv(ZKpl+Pmg2DWLAxqWz9bapL5EHpxQE8O4>hY z|5*>UR?EsjbKUQS-9miyVe1>AjaRmeK-h;%Yr^j#97otkB(GewA#`KrSnq_i0~=v(W))J7KYChTsZUGO1$(E4#2$ zQEyMVGV?v+Z9`y}1a*@m4`I!rYRNRyP)Ai0#YNWE8MjE`ENtnuK@;T`D?;Oi}$ z^EO2Lx%V#Ab@ULJ4hUw40cRILm}ry*6+PIo0Ub%@x8GG96`2SULR$c0`Y@8YdTyN^ zP=#}Iz>8KrQYE!c^mc=&dfq@!5%^!iOtt*%s01%%5s;74Xg^y!y$4NUp z)E$DXQ+}RpskQjlqB~b9+)dzH;QQ0>iv_?BxZj5!V{=>@O`nGJkYuWTE;h?`>_v+# zSc4;#F5y7LOi<#=kE$PV!F!;piB{Ki&P6fqnr0GUGRlHYD3NkuCE3!qQ)o#Eo8@+Y)UzuDjTu8+>eNsP$=w&4cTO50^XsX zN(u@C8BQ^|&W;U*23c>`8tC3u>_@t*K(t;1#EgBXi&ShHT~rwd`a{d(nzyZBdp@g?Plyl=8z1zXZ4Ic*^V4t zkC^ew-#dgx@78lV^YGHOI3i-_@m~;S9z3m!Z@BBiMCe#R^oBilw<75Rgd0`j?fw(| z5&Ql9dPIl!ucz&6AIpq(T>GCQT*~tnPmD4o zx3*uB$E zH%@6ERj%JI%rtoI&QH`P zRA7%W_W;d)NqSFPg6X(i(yTtB*sIs?|9q?)3&qd?yKJ4PETtQLS{>`6)xQ0V`))&7 zaLefPWtXt##4lN6>fc}l?&||5%a4Pl;P9BGC+4TGupLSpFm8fD+CF3lF0&Q-r7Oo$qD6haE^8iOp{?kaVWZ zVY;hSpDJW}_NAP5+ZPd1Ox&WjdPPvvCX3yRAweUiW3c`=P{t}YXx<LF7lgj zlTOY3XGg1G#+TyQ%Z|W-KRfwQ>w%QW(=~Il(aAve0nX!G|;bIy16;+I}Cxe>jnHxK~~k6&e|2Gp5l+-zd~B)d(uPpP2Lbzh9S? zXofhqRNbdyda}2ehusl3OYUTT7RsD(zNtw6(Szp3nM6MIHU{u(F8 zG{-cg;hTN6H=ZXktMFw2Yxv^hewRMdk+;6Q;h0?p!Ni<=hY6>ilAtY-X!(cLTAd1V zEK*%Vi1!5BkVbo=v%!1E3O@Wy8Nsy7!6*7SITprWed-{`CGc#bv*a3FgpI+{F1H>lRWN3PU znR3t`9ra#FvTta4V)F{L_-VH)ti*Pk5@TwFw))sG8Jxn-*!rQ>@g{gr*j9Oea zS-T_=aZDac{oLP)^+*dXo*oSJ;?ISXI)pOq?bNga83!hLJn5^DD2N=RVesr&WDZ!2 zsYz}O9nnVCNP~Cz)heLAr7EdLys@eQfpZ3LUQ%<2ghRLmqHM88q&N=fwY!KU@2Yhw z>JgUwa#9WY!Wk9&P9uI{NBS&@>>!Jli^H-BvflkV>AIF$LV0{XLC$!n*HBish{(=- zoxIiIiM&jH2BC^R>Rb5oVnY%BP2TR91^)qI+zT5Z}}^EUR`|ke&|jl2VPQB)8ooB4W7=>bCdl^4MuwXs>q@ z7hJLtoJbGa%apsA*t?-+OV{Wq$a23l?Oo9sCdSM%p56@e(hNtZZ%la}HE0RAK&%l5 zJUu*DVZAR?wHeQMiZ9_E-2u!lI)`fK;p|*;rg+gxR=o%f13T?xdr*i~A0JkfOBvVi zGBb|)MFF;B5Sd7$UO;Rpl>{1Kc zLO}cN6yxoCGnjQ8(9k-+ZTbhOhdp!qxQ9b`cTmhH)**Z~0KYV5ha@7MlhnXgvMdiR zO6FE(WmNyx&g>R&U!SzqCwep0Z#C*tbv5pXJi#q_Iz_dw$jP^->WneRKy*|LEpv^| zs9rIyVi@&;`L#P5&WP&O^;c#3Eu_W0(+@Ka&fQ1c|J9*3n5TyTmJ9)OSCkE~;xBT; zHb#orw}6a4;V18d!!LnbA90xQ51{&<7B?2?RgxNC(Wcm!VEon=Sa*@YR7-Bas7LjC zpp1CfxNoLjNH3Rq4p$&G*LKj0G8t{J?Gro6HqjAg>|5-Yg1TBx)38Q(c^`FsP%RH zLT&|-_Or;OOi0uj24{L+8oM|0dxXArgGA8)@IsGxhpJEK*u93edX>@IcbyX(V<$zxW1jtv{ zsOQr}&-oJVNlY-b=aY6RGy~`dg}3oLVt2P8qNt zdth^#44PWG?|k7ydD`lD21_v}O|XblRT<26EqYZ?4$&VX6mg`h3E=e24{8Jf$0^#ED+y1*!u>sv^hCC#mZ;Ay%LKb^vw`b z^tcFcjZx5BL*k70?SO(}tW}+S$*f}Jhul8)_uY`lLb;h5LlexZf*-6Y+P3MjYzm%x zI~>--uLF$!fO{KY;j4GjL)R-mdKkxH_*J$NG! z>WN$9y(}pbzI0)QM%gxy|MA4o6fD^>aUc4PqNUkGua(5w3?*;XUnOWiVR%yC_t7-q zq2`lIf%~=R7D~*tg%NF{KR4SsjAue^L)1=@D3P#6MRu5z??ufqG$xVpQAlE8 zjxS3RX`tjTr^w+j52uA}dI}p~C~Kg>+ALw91bp8eppdsB&8Vro+3h<&v1HMRijh1e zE+?MCrC#3ib0V$u;1k0Op)lAK%;Y={pV@e)kuZhqt& zzlt;0Fhev=rQE>?$jG-tC#7N5@8(4atvFBtisXe!Ifd2}UG&!axy~hPGMN;DQPqw6 zHl#%uPiqz{N4srn$>g8kGN}d4Ed6>s$Q|Cdd|I-hL)zzkC1Xojm zvG)o$IkxwAh_yTE$MxIC z?cenfV0-d0%UB9BD!m&QANYI2v0;A}7cNqe{ymBan$&*70b1{jVSI$$9m@}*#@j{Q z=G15Tn7eyN>_v+D@Z!7fLS4FC=gA>**C;=1s?7w%F0 zB(0R~U|{y)u~c7=ONz)O5Nqy+MU6VCq9Lu_wvz=`WT$lrl}k*lBPY<;DIvuE9HaX4 z)y+b5p||_z@(MC5?;wx4(3ff%1x)@(v8VIDK=thEZSF7wa6?~?{?r$Q%1o7iA&(q@ z9%AXqy_F(+!XPEBk$ZXY+$Rr;#nwPL&-qAX3#+@XXMf?|1yb=R72h^G*!P(|cz!Jc zHh7*AYQ1JU%4(Uai>dlA z-M%QbZtW;hMx%%=#wX7uJ+o5l zmcY?Nxc|_3v(3x&6+|iP`K*&(Pp^YrQVs25ax_X%z#)if?+0dPwtdC`Ez;Fd_I>3xm`Cxc}A`zG9Hcld4waN8Sj#8R}oi9^&S0&1b2oT-qv z%LRE>yJ+yer`*?GANTf!oi&YqEq^$h{k2uyq@oX6$ZGXB%PHNg7o{k~ozOEIUlsWh zWJ+%KCyS0cK*Ns5MEsdkQ&r$@Eyd-?jYZ*&`q#F=%dh6A9mql0Tq*mkWQv7Mc=;gp z@X>KN3)Ea@FIN^##P2xg1}&tZ;{~JTg%};~exU?0OFsKo6YlaR;|Q2W(L%2c+N$RR z>m@>jgp_WyGC!zG+?d*VR)E7zSJvf`Ffvb(tSL}UMT;cD_^gWNJhU_iX3>Tt@kCwJ; zw!;4G7R%kZtEFHsg~LW5b6F97m$dx1ZNH?!cSe7kz)^VX*Up&cj_#F zjy5@m-<~7Xjrdf7n}^dFULM+)K&xkVS(glQUR)9WA)fYJ%Y3Ri-o5!t)MWPLGIbb* z<0Pn@M3YBdIQKZ%t%?=Cd%?28M77X55;!{V*f4+ACV}h31AJWlV8G*pbnsH;`AxT# zYb%+tbTxQ!`sY#c^Rx6{iU$B7!nKn49>IP4FUueQ-v`(^JJ`AU___OV1%ce%{y8e( z-&!O75B(oJ0)o8%p#KR7@%~%?`Lf_JXi`(lEYRuDSO(s^Ru%)2&1%Y z6fMMGnG-6Cngp6gJrDi6EVrzzbq=o%f?q1SzHYyqyqrG76VAV9lJWF0{r%0P!#P0^ zsxjYc>94>P=faXi9UWJ~piI+CTi+E9d#v=-G--%=pOB!6%h0SV5K3>z-LjY2o!TfI z%($}TM`JH7;sBM_V%GTCFJ?l~5;N}h9*XS$_FJg-+2IdS)Y2%h3I?BMpbYZ9QKSHg zv4}k{t5CY89nlqWLBcqeuAy2;Vao8=qz?aw$Go&s|9~o_phO{UzY5#!*YBHes5&xa z)Nw2xoJGc?B7_&UT>}*&q X!2e5X@$W Date: Wed, 28 May 2025 10:51:53 -0400 Subject: [PATCH 03/17] removed comments --- lib/model_context_protocol.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/model_context_protocol.rb b/lib/model_context_protocol.rb index a92376f..ad3eae8 100644 --- a/lib/model_context_protocol.rb +++ b/lib/model_context_protocol.rb @@ -16,13 +16,6 @@ require_relative "model_context_protocol/server" require_relative "model_context_protocol/server/transports/stdio" -# Client files will be added here once implemented -# require_relative "model_context_protocol/client/client" -# require_relative "model_context_protocol/client/transports/websocket" -# require_relative "model_context_protocol/client/transports/http" -# require_relative "model_context_protocol/client/auth/oauth_client" -# require_relative "model_context_protocol/client/auth/token_storage" - module ModelContextProtocol class Error < StandardError; end From 5bd5d25fde4d542cd25782b95d7432c421bb0280 Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 10:58:33 -0400 Subject: [PATCH 04/17] fix namespacing --- README.md | 2 +- examples/stdio_server.rb | 2 +- .../server/transports/stdio.rb | 45 ++++++++++--------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 00acd40..cbb0cc0 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ server = ModelContextProtocol::Server.new( ) # Create and start the transport -transport = ModelContextProtocol::Transports::StdioTransport.new(server) +transport = ModelContextProtocol::Server::Transports::StdioTransport.new(server) transport.open ``` diff --git a/examples/stdio_server.rb b/examples/stdio_server.rb index 631822e..42db28f 100755 --- a/examples/stdio_server.rb +++ b/examples/stdio_server.rb @@ -91,5 +91,5 @@ def template(args, server_context:) end # Create and start the transport -transport = MCP::Transports::StdioTransport.new(server) +transport = ModelContextProtocol::Server::Transports::StdioTransport.new(server) transport.open diff --git a/lib/model_context_protocol/server/transports/stdio.rb b/lib/model_context_protocol/server/transports/stdio.rb index bb499f3..097b87c 100644 --- a/lib/model_context_protocol/server/transports/stdio.rb +++ b/lib/model_context_protocol/server/transports/stdio.rb @@ -3,33 +3,34 @@ require_relative "../../shared/transport" require "json" -# TODO: change class name module ModelContextProtocol - module Transports - class StdioTransport < Transport - def initialize(server) - @server = server - @open = false - $stdin.set_encoding("UTF-8") - $stdout.set_encoding("UTF-8") - super - end + class Server + module Transports + class StdioTransport < Transport + def initialize(server) + @server = server + @open = false + $stdin.set_encoding("UTF-8") + $stdout.set_encoding("UTF-8") + super + end - def open - @open = true - while @open && (line = $stdin.gets) - handle_json_request(line.strip) + def open + @open = true + while @open && (line = $stdin.gets) + handle_json_request(line.strip) + end end - end - def close - @open = false - end + def close + @open = false + end - def send_response(message) - json_message = message.is_a?(String) ? message : JSON.generate(message) - $stdout.puts(json_message) - $stdout.flush + def send_response(message) + json_message = message.is_a?(String) ? message : JSON.generate(message) + $stdout.puts(json_message) + $stdout.flush + end end end end From f5a42928a2b5a31c0a67bef2fec60ffd5214b059 Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 11:01:18 -0400 Subject: [PATCH 05/17] drop unnecsessary change --- lib/model_context_protocol.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/model_context_protocol.rb b/lib/model_context_protocol.rb index ad3eae8..83afe4e 100644 --- a/lib/model_context_protocol.rb +++ b/lib/model_context_protocol.rb @@ -17,8 +17,6 @@ require_relative "model_context_protocol/server/transports/stdio" module ModelContextProtocol - class Error < StandardError; end - class << self def configure yield(configuration) From fad37ebc94e20fc422b99aaa2c3e41e94de07f3b Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 11:02:27 -0400 Subject: [PATCH 06/17] remove DS_Store --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 3 +++ 2 files changed, 3 insertions(+) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Wed, 28 May 2025 11:05:25 -0400 Subject: [PATCH 07/17] add original spec files code back --- model_context_protocol.gemspec | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/model_context_protocol.gemspec b/model_context_protocol.gemspec index 9892d91..7c47ad0 100644 --- a/model_context_protocol.gemspec +++ b/model_context_protocol.gemspec @@ -19,7 +19,9 @@ Gem::Specification.new do |spec| spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage - spec.files = Dir.glob("lib/**/*.rb").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do + %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } From c3a4b80fb1962f61fb0e3ad23395a25ba296853a Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 11:42:38 -0400 Subject: [PATCH 08/17] fix imports --- lib/model_context_protocol.rb | 12 ++++++++++-- lib/model_context_protocol/shared/tool.rb | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/model_context_protocol.rb b/lib/model_context_protocol.rb index 83afe4e..ed46ab9 100644 --- a/lib/model_context_protocol.rb +++ b/lib/model_context_protocol.rb @@ -8,11 +8,19 @@ require_relative "model_context_protocol/shared/transport" require_relative "model_context_protocol/shared/content" require_relative "model_context_protocol/shared/string_utils" -require_relative "model_context_protocol/shared/tool" -require_relative "model_context_protocol/shared/prompt" require_relative "model_context_protocol/shared/resource" require_relative "model_context_protocol/shared/resource_template" +require_relative "model_context_protocol/shared/tool" +require_relative "model_context_protocol/shared/tool/input_schema" +require_relative "model_context_protocol/shared/tool/response" +require_relative "model_context_protocol/shared/tool/annotations" + +require_relative "model_context_protocol/shared/prompt" +require_relative "model_context_protocol/shared/prompt/argument" +require_relative "model_context_protocol/shared/prompt/message" +require_relative "model_context_protocol/shared/prompt/result" + require_relative "model_context_protocol/server" require_relative "model_context_protocol/server/transports/stdio" diff --git a/lib/model_context_protocol/shared/tool.rb b/lib/model_context_protocol/shared/tool.rb index 51b5d63..87b684b 100644 --- a/lib/model_context_protocol/shared/tool.rb +++ b/lib/model_context_protocol/shared/tool.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require_relative "tool/input_schema" +require_relative "tool/response" +require_relative "tool/annotations" module ModelContextProtocol class Tool From c780241fcc42b8a358c8dba6ec17be82b9d00839 Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 11:47:51 -0400 Subject: [PATCH 09/17] remove extra require statements --- lib/model_context_protocol/shared/prompt.rb | 4 ---- lib/model_context_protocol/shared/tool.rb | 4 ---- 2 files changed, 8 deletions(-) diff --git a/lib/model_context_protocol/shared/prompt.rb b/lib/model_context_protocol/shared/prompt.rb index 7633c66..d7cd4b9 100644 --- a/lib/model_context_protocol/shared/prompt.rb +++ b/lib/model_context_protocol/shared/prompt.rb @@ -1,10 +1,6 @@ # typed: strict # frozen_string_literal: true -require_relative "prompt/argument" -require_relative "prompt/message" -require_relative "prompt/result" - module ModelContextProtocol class Prompt class << self diff --git a/lib/model_context_protocol/shared/tool.rb b/lib/model_context_protocol/shared/tool.rb index 87b684b..388cf88 100644 --- a/lib/model_context_protocol/shared/tool.rb +++ b/lib/model_context_protocol/shared/tool.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -require_relative "tool/input_schema" -require_relative "tool/response" -require_relative "tool/annotations" - module ModelContextProtocol class Tool class << self From 477b007b46befa06eb53f97b64654d756ad09d91 Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 12:14:29 -0400 Subject: [PATCH 10/17] move resource stuff --- lib/model_context_protocol.rb | 3 +++ lib/model_context_protocol/{ => shared}/resource/contents.rb | 0 lib/model_context_protocol/{ => shared}/resource/embedded.rb | 0 3 files changed, 3 insertions(+) rename lib/model_context_protocol/{ => shared}/resource/contents.rb (100%) rename lib/model_context_protocol/{ => shared}/resource/embedded.rb (100%) diff --git a/lib/model_context_protocol.rb b/lib/model_context_protocol.rb index ed46ab9..7cb2ff4 100644 --- a/lib/model_context_protocol.rb +++ b/lib/model_context_protocol.rb @@ -8,7 +8,10 @@ require_relative "model_context_protocol/shared/transport" require_relative "model_context_protocol/shared/content" require_relative "model_context_protocol/shared/string_utils" + require_relative "model_context_protocol/shared/resource" +require_relative "model_context_protocol/shared/resource/contents" +require_relative "model_context_protocol/shared/resource/embedded" require_relative "model_context_protocol/shared/resource_template" require_relative "model_context_protocol/shared/tool" diff --git a/lib/model_context_protocol/resource/contents.rb b/lib/model_context_protocol/shared/resource/contents.rb similarity index 100% rename from lib/model_context_protocol/resource/contents.rb rename to lib/model_context_protocol/shared/resource/contents.rb diff --git a/lib/model_context_protocol/resource/embedded.rb b/lib/model_context_protocol/shared/resource/embedded.rb similarity index 100% rename from lib/model_context_protocol/resource/embedded.rb rename to lib/model_context_protocol/shared/resource/embedded.rb From 29771c5446c748a542b0898e07d864694411e0ab Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 12:19:56 -0400 Subject: [PATCH 11/17] move and fix some tests --- examples/stdio_server.rb | 2 +- .../server/transports/stdio_transport_test.rb | 127 ++++++++++++++++++ .../{ => shared}/configuration_test.rb | 0 .../{ => shared}/instrumentation_test.rb | 0 .../{ => shared}/methods_test.rb | 0 .../{ => shared}/prompt_test.rb | 0 .../{ => shared}/string_utils_test.rb | 0 .../{ => shared}/tool/input_schema_test.rb | 0 .../{ => shared}/tool_test.rb | 0 .../transports/stdio_transport_test.rb | 125 ----------------- 10 files changed, 128 insertions(+), 126 deletions(-) create mode 100644 test/model_context_protocol/server/transports/stdio_transport_test.rb rename test/model_context_protocol/{ => shared}/configuration_test.rb (100%) rename test/model_context_protocol/{ => shared}/instrumentation_test.rb (100%) rename test/model_context_protocol/{ => shared}/methods_test.rb (100%) rename test/model_context_protocol/{ => shared}/prompt_test.rb (100%) rename test/model_context_protocol/{ => shared}/string_utils_test.rb (100%) rename test/model_context_protocol/{ => shared}/tool/input_schema_test.rb (100%) rename test/model_context_protocol/{ => shared}/tool_test.rb (100%) delete mode 100644 test/model_context_protocol/transports/stdio_transport_test.rb diff --git a/examples/stdio_server.rb b/examples/stdio_server.rb index 42db28f..bf29eb3 100755 --- a/examples/stdio_server.rb +++ b/examples/stdio_server.rb @@ -3,7 +3,7 @@ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) require "model_context_protocol" -require "model_context_protocol/transports/stdio" +require "model_context_protocol/server/transports/stdio" # Create a simple tool class ExampleTool < MCP::Tool diff --git a/test/model_context_protocol/server/transports/stdio_transport_test.rb b/test/model_context_protocol/server/transports/stdio_transport_test.rb new file mode 100644 index 0000000..2b05d5f --- /dev/null +++ b/test/model_context_protocol/server/transports/stdio_transport_test.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require "test_helper" +require "model_context_protocol/server/transports/stdio" +require "json" + +module ModelContextProtocol + class Server + module Transports + class StdioTransportTest < ActiveSupport::TestCase + include InstrumentationTestHelper + + setup do + configuration = ModelContextProtocol::Configuration.new + configuration.instrumentation_callback = instrumentation_helper.callback + @server = Server.new(name: "test_server", configuration: configuration) + @transport = StdioTransport.new(@server) + end + + test "initializes with server and closed state" do + server = @transport.instance_variable_get(:@server) + assert_equal @server.object_id, server.object_id + refute @transport.instance_variable_get(:@open) + end + + test "processes JSON-RPC requests from stdin and sends responses to stdout" do + request = { + jsonrpc: "2.0", + method: "ping", + id: "123", + } + input = StringIO.new(JSON.generate(request) + "\n") + output = StringIO.new + + original_stdin = $stdin + original_stdout = $stdout + + begin + $stdin = input + $stdout = output + + thread = Thread.new { @transport.open } + sleep(0.1) + @transport.close + thread.join + + response = JSON.parse(output.string, symbolize_names: true) + assert_equal("2.0", response[:jsonrpc]) + assert_equal("123", response[:id]) + assert_equal({}, response[:result]) + refute(@transport.instance_variable_get(:@open)) + ensure + $stdin = original_stdin + $stdout = original_stdout + end + end + + test "sends string responses to stdout" do + output = StringIO.new + original_stdout = $stdout + + begin + $stdout = output + @transport.send_response("test response") + assert_equal("test response\n", output.string) + ensure + $stdout = original_stdout + end + end + + test "sends JSON responses to stdout" do + output = StringIO.new + original_stdout = $stdout + + begin + $stdout = output + response = { key: "value" } + @transport.send_response(response) + assert_equal(JSON.generate(response) + "\n", output.string) + ensure + $stdout = original_stdout + end + end + + test "handles valid JSON-RPC requests" do + request = { + jsonrpc: "2.0", + method: "ping", + id: "123", + } + output = StringIO.new + original_stdout = $stdout + + begin + $stdout = output + @transport.send(:handle_request, JSON.generate(request)) + response = JSON.parse(output.string, symbolize_names: true) + assert_equal("2.0", response[:jsonrpc]) + assert_nil(response[:id]) + assert_nil(response[:result]) + ensure + $stdout = original_stdout + end + end + + test "handles invalid JSON requests" do + invalid_json = "invalid json" + output = StringIO.new + original_stdout = $stdout + + begin + $stdout = output + @transport.send(:handle_request, invalid_json) + response = JSON.parse(output.string, symbolize_names: true) + assert_equal("2.0", response[:jsonrpc]) + assert_nil(response[:id]) + assert_equal(-32600, response[:error][:code]) + assert_equal("Invalid Request", response[:error][:message]) + assert_equal("Request must be an array or a hash", response[:error][:data]) + ensure + $stdout = original_stdout + end + end + end + end + end +end diff --git a/test/model_context_protocol/configuration_test.rb b/test/model_context_protocol/shared/configuration_test.rb similarity index 100% rename from test/model_context_protocol/configuration_test.rb rename to test/model_context_protocol/shared/configuration_test.rb diff --git a/test/model_context_protocol/instrumentation_test.rb b/test/model_context_protocol/shared/instrumentation_test.rb similarity index 100% rename from test/model_context_protocol/instrumentation_test.rb rename to test/model_context_protocol/shared/instrumentation_test.rb diff --git a/test/model_context_protocol/methods_test.rb b/test/model_context_protocol/shared/methods_test.rb similarity index 100% rename from test/model_context_protocol/methods_test.rb rename to test/model_context_protocol/shared/methods_test.rb diff --git a/test/model_context_protocol/prompt_test.rb b/test/model_context_protocol/shared/prompt_test.rb similarity index 100% rename from test/model_context_protocol/prompt_test.rb rename to test/model_context_protocol/shared/prompt_test.rb diff --git a/test/model_context_protocol/string_utils_test.rb b/test/model_context_protocol/shared/string_utils_test.rb similarity index 100% rename from test/model_context_protocol/string_utils_test.rb rename to test/model_context_protocol/shared/string_utils_test.rb diff --git a/test/model_context_protocol/tool/input_schema_test.rb b/test/model_context_protocol/shared/tool/input_schema_test.rb similarity index 100% rename from test/model_context_protocol/tool/input_schema_test.rb rename to test/model_context_protocol/shared/tool/input_schema_test.rb diff --git a/test/model_context_protocol/tool_test.rb b/test/model_context_protocol/shared/tool_test.rb similarity index 100% rename from test/model_context_protocol/tool_test.rb rename to test/model_context_protocol/shared/tool_test.rb diff --git a/test/model_context_protocol/transports/stdio_transport_test.rb b/test/model_context_protocol/transports/stdio_transport_test.rb deleted file mode 100644 index 498b0ff..0000000 --- a/test/model_context_protocol/transports/stdio_transport_test.rb +++ /dev/null @@ -1,125 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "model_context_protocol/transports/stdio" -require "json" - -module ModelContextProtocol - module Transports - class StdioTransportTest < ActiveSupport::TestCase - include InstrumentationTestHelper - - setup do - configuration = ModelContextProtocol::Configuration.new - configuration.instrumentation_callback = instrumentation_helper.callback - @server = Server.new(name: "test_server", configuration: configuration) - @transport = StdioTransport.new(@server) - end - - test "initializes with server and closed state" do - server = @transport.instance_variable_get(:@server) - assert_equal @server.object_id, server.object_id - refute @transport.instance_variable_get(:@open) - end - - test "processes JSON-RPC requests from stdin and sends responses to stdout" do - request = { - jsonrpc: "2.0", - method: "ping", - id: "123", - } - input = StringIO.new(JSON.generate(request) + "\n") - output = StringIO.new - - original_stdin = $stdin - original_stdout = $stdout - - begin - $stdin = input - $stdout = output - - thread = Thread.new { @transport.open } - sleep(0.1) - @transport.close - thread.join - - response = JSON.parse(output.string, symbolize_names: true) - assert_equal("2.0", response[:jsonrpc]) - assert_equal("123", response[:id]) - assert_equal({}, response[:result]) - refute(@transport.instance_variable_get(:@open)) - ensure - $stdin = original_stdin - $stdout = original_stdout - end - end - - test "sends string responses to stdout" do - output = StringIO.new - original_stdout = $stdout - - begin - $stdout = output - @transport.send_response("test response") - assert_equal("test response\n", output.string) - ensure - $stdout = original_stdout - end - end - - test "sends JSON responses to stdout" do - output = StringIO.new - original_stdout = $stdout - - begin - $stdout = output - response = { key: "value" } - @transport.send_response(response) - assert_equal(JSON.generate(response) + "\n", output.string) - ensure - $stdout = original_stdout - end - end - - test "handles valid JSON-RPC requests" do - request = { - jsonrpc: "2.0", - method: "ping", - id: "123", - } - output = StringIO.new - original_stdout = $stdout - - begin - $stdout = output - @transport.send(:handle_request, JSON.generate(request)) - response = JSON.parse(output.string, symbolize_names: true) - assert_equal("2.0", response[:jsonrpc]) - assert_nil(response[:id]) - assert_nil(response[:result]) - ensure - $stdout = original_stdout - end - end - - test "handles invalid JSON requests" do - invalid_json = "invalid json" - output = StringIO.new - original_stdout = $stdout - - begin - $stdout = output - @transport.send(:handle_request, invalid_json) - response = JSON.parse(output.string, symbolize_names: true) - assert_equal("2.0", response[:jsonrpc]) - assert_nil(response[:id]) - assert_equal(-32600, response[:error][:code]) - assert_equal("Invalid Request", response[:error][:message]) - assert_equal("Request must be an array or a hash", response[:error][:data]) - ensure - $stdout = original_stdout - end - end - end - end -end From 9e30a0a929ba88c42cf26db9741341b49034ed29 Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 12:23:42 -0400 Subject: [PATCH 12/17] anotha fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbb0cc0..09b5c17 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ If you want to build a local command-line application, you can use the stdio tra ```ruby #!/usr/bin/env ruby require "model_context_protocol" -require "model_context_protocol/transports/stdio" +require "model_context_protocol/server/transports/stdio" # Create a simple tool class ExampleTool < ModelContextProtocol::Tool From c5b325e09fd3d613c83b8d8fa64b5b9bfb2279ba Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 16:35:00 -0400 Subject: [PATCH 13/17] Add basic HTTP client support --- Gemfile | 4 + lib/model_context_protocol.rb | 5 + lib/model_context_protocol/client.rb | 11 ++ lib/model_context_protocol/client/http.rb | 61 ++++++ lib/model_context_protocol/client/tool.rb | 26 +++ lib/model_context_protocol/client/tools.rb | 30 +++ model_context_protocol.gemspec | 1 + .../client/http_test.rb | 186 ++++++++++++++++++ .../client/tool_test.rb | 46 +++++ .../client/tools_test.rb | 96 +++++++++ test/model_context_protocol/client_test.rb | 8 + 11 files changed, 474 insertions(+) create mode 100644 lib/model_context_protocol/client.rb create mode 100644 lib/model_context_protocol/client/http.rb create mode 100644 lib/model_context_protocol/client/tool.rb create mode 100644 lib/model_context_protocol/client/tools.rb create mode 100644 test/model_context_protocol/client/http_test.rb create mode 100644 test/model_context_protocol/client/tool_test.rb create mode 100644 test/model_context_protocol/client/tools_test.rb create mode 100644 test/model_context_protocol/client_test.rb diff --git a/Gemfile b/Gemfile index ecf15d1..e087018 100644 --- a/Gemfile +++ b/Gemfile @@ -13,3 +13,7 @@ gem "rubocop-shopify", require: false gem "minitest-reporters" gem "mocha" gem "debug" + +group :test do + gem "webmock" +end diff --git a/lib/model_context_protocol.rb b/lib/model_context_protocol.rb index 7cb2ff4..dab8c70 100644 --- a/lib/model_context_protocol.rb +++ b/lib/model_context_protocol.rb @@ -27,6 +27,11 @@ require_relative "model_context_protocol/server" require_relative "model_context_protocol/server/transports/stdio" +require_relative "model_context_protocol/client" +require_relative "model_context_protocol/client/http" +require_relative "model_context_protocol/client/tools" +require_relative "model_context_protocol/client/tool" + module ModelContextProtocol class << self def configure diff --git a/lib/model_context_protocol/client.rb b/lib/model_context_protocol/client.rb new file mode 100644 index 0000000..b321f7a --- /dev/null +++ b/lib/model_context_protocol/client.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# require "json_rpc_handler" +# require_relative "shared/instrumentation" +# require_relative "shared/methods" + +module ModelContextProtocol + module Client + # Can be made an abstract class if we need shared behavior + end +end diff --git a/lib/model_context_protocol/client/http.rb b/lib/model_context_protocol/client/http.rb new file mode 100644 index 0000000..714fc5f --- /dev/null +++ b/lib/model_context_protocol/client/http.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# require "json_rpc_handler" +# require_relative "shared/instrumentation" +# require_relative "shared/methods" + +module ModelContextProtocol + module Client + class Http + DEFAULT_VERSION = "0.1.0" + + attr_reader :url, :version + + def initialize(url:, version: DEFAULT_VERSION) + @url = url + @version = version + end + + def tools + response = client.post( + "", + method: "tools/list", + jsonrpc: "2.0", + id: request_id, + mcp: { method: "tools/list", jsonrpc: "2.0", id: request_id }, + ).body + + ::ModelContextProtocol::Client::Tools.new(response) + end + + def call_tool(tool:, input:) + response = client.post( + "", + { + jsonrpc: "2.0", + id: request_id, + method: "tools/call", + params: { name: tool.name, arguments: input }, + mcp: { jsonrpc: "2.0", id: request_id, method: "tools/call", params: { name: tool.name, arguments: input } }, + }, + ).body + + response.dig("result", "content", 0, "text") + end + + private + + def client + @client ||= Faraday.new(url) do |faraday| + faraday.request(:json) + faraday.response(:json) + # TODO: error middleware? + end + end + + def request_id + SecureRandom.uuid_v7 + end + end + end +end diff --git a/lib/model_context_protocol/client/tool.rb b/lib/model_context_protocol/client/tool.rb new file mode 100644 index 0000000..156a5b7 --- /dev/null +++ b/lib/model_context_protocol/client/tool.rb @@ -0,0 +1,26 @@ +# typed: false +# frozen_string_literal: true + +module ModelContextProtocol + module Client + class Tool + attr_reader :payload + + def initialize(payload) + @payload = payload + end + + def name + payload["name"] + end + + def description + payload["description"] + end + + def input_schema + payload["inputSchema"] + end + end + end +end diff --git a/lib/model_context_protocol/client/tools.rb b/lib/model_context_protocol/client/tools.rb new file mode 100644 index 0000000..a63f33f --- /dev/null +++ b/lib/model_context_protocol/client/tools.rb @@ -0,0 +1,30 @@ +# typed: false +# frozen_string_literal: true + +module ModelContextProtocol + module Client + class Tools + include Enumerable + + attr_reader :response + + def initialize(response) + @response = response + end + + def each(&block) + tools.each(&block) + end + + def all + tools + end + + private + + def tools + @tools ||= @response.dig("result", "tools")&.map { |tool| Tool.new(tool) } || [] + end + end + end +end diff --git a/model_context_protocol.gemspec b/model_context_protocol.gemspec index 7c47ad0..6014bb5 100644 --- a/model_context_protocol.gemspec +++ b/model_context_protocol.gemspec @@ -27,6 +27,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] + spec.add_dependency("faraday", ">= 2.0") spec.add_dependency("json_rpc_handler", "~> 0.1") spec.add_development_dependency("activesupport") spec.add_development_dependency("sorbet-static-and-runtime") diff --git a/test/model_context_protocol/client/http_test.rb b/test/model_context_protocol/client/http_test.rb new file mode 100644 index 0000000..0fd4413 --- /dev/null +++ b/test/model_context_protocol/client/http_test.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +require "test_helper" +require "faraday" +require "securerandom" +require "webmock/minitest" + +module ModelContextProtocol + module Client + class HttpTest < Minitest::Test + def test_initialization_with_default_version + assert_equal("0.1.0", client.version) + assert_equal(url, client.url) + end + + def test_initialization_with_custom_version + custom_version = "1.2.3" + client = Http.new(url:, version: custom_version) + assert_equal(custom_version, client.version) + end + + def test_tools_returns_tools_instance + stub_request(:post, url) + .with( + body: { + method: "tools/list", + jsonrpc: "2.0", + id: mock_request_id, + mcp: { + method: "tools/list", + jsonrpc: "2.0", + id: mock_request_id, + }, + }, + ) + .to_return( + status: 200, + headers: { + "Content-Type" => "application/json", + }, + body: { + result: { + tools: [ + { + name: "test_tool", + description: "A test tool", + inputSchema: { + type: "object", + properties: {}, + }, + }, + ], + }, + }.to_json, + ) + + tools = client.tools + assert_instance_of(Tools, tools) + assert_equal(1, tools.count) + assert_equal("test_tool", tools.first.name) + end + + def test_call_tool_returns_tool_response + tool = Tool.new( + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { + "type" => "object", + "properties" => {}, + }, + ) + input = { "param" => "value" } + + stub_request(:post, url) + .with( + body: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + mcp: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + }, + }, + ) + .to_return( + status: 200, + headers: { + "Content-Type" => "application/json", + }, + body: { + result: { + content: [ + { + text: "Tool response", + }, + ], + }, + }.to_json, + ) + + response = client.call_tool(tool: tool, input: input) + assert_equal("Tool response", response) + end + + def test_call_tool_handles_empty_response + tool = Tool.new( + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { + "type" => "object", + "properties" => {}, + }, + ) + input = { "param" => "value" } + + stub_request(:post, url) + .with( + body: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + mcp: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + }, + }, + ) + .to_return( + status: 200, + headers: { + "Content-Type" => "application/json", + }, + body: { + result: { + content: [], + }, + }.to_json, + ) + + response = client.call_tool(tool: tool, input: input) + assert_nil(response) + end + + private + + def stub_request(method, url) + WebMock.stub_request(method, url) + end + + def mock_request_id + "random_request_id" + end + + def url + "http://example.com" + end + + def client + @client ||= begin + client = Http.new(url:) + client.stubs(:request_id).returns(mock_request_id) + client + end + end + end + end +end diff --git a/test/model_context_protocol/client/tool_test.rb b/test/model_context_protocol/client/tool_test.rb new file mode 100644 index 0000000..6dbcbc1 --- /dev/null +++ b/test/model_context_protocol/client/tool_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "test_helper" + +module ModelContextProtocol + module Client + class ToolTest < Minitest::Test + def test_name_returns_name_from_payload + tool = Tool.new("name" => "test_tool") + assert_equal("test_tool", tool.name) + end + + def test_name_returns_nil_when_not_in_payload + tool = Tool.new({}) + assert_nil(tool.name) + end + + def test_description_returns_description_from_payload + tool = Tool.new("description" => "A test tool") + assert_equal("A test tool", tool.description) + end + + def test_description_returns_nil_when_not_in_payload + tool = Tool.new({}) + assert_nil(tool.description) + end + + def test_input_schema_returns_input_schema_from_payload + schema = { "type" => "object", "properties" => { "foo" => { "type" => "string" } } } + tool = Tool.new("inputSchema" => schema) + assert_equal(schema, tool.input_schema) + end + + def test_input_schema_returns_nil_when_not_in_payload + tool = Tool.new({}) + assert_nil(tool.input_schema) + end + + def test_payload_is_accessible + payload = { "name" => "test", "description" => "desc", "inputSchema" => {} } + tool = Tool.new(payload) + assert_equal(payload, tool.payload) + end + end + end +end diff --git a/test/model_context_protocol/client/tools_test.rb b/test/model_context_protocol/client/tools_test.rb new file mode 100644 index 0000000..c832ecd --- /dev/null +++ b/test/model_context_protocol/client/tools_test.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "test_helper" + +module ModelContextProtocol + module Client + class ToolsTest < Minitest::Test + def test_each_iterates_over_tools + response = { + "result" => { + "tools" => [ + { "name" => "tool1", "description" => "First tool" }, + { "name" => "tool2", "description" => "Second tool" }, + ], + }, + } + tools = Tools.new(response) + + tool_names = [] + tools.each { |tool| tool_names << tool.name } + + assert_equal(["tool1", "tool2"], tool_names) + end + + def test_all_returns_array_of_tools + response = { + "result" => { + "tools" => [ + { "name" => "tool1", "description" => "First tool" }, + { "name" => "tool2", "description" => "Second tool" }, + ], + }, + } + tools = Tools.new(response) + + all_tools = tools.all + assert_equal(2, all_tools.length) + assert(all_tools.all? { |tool| tool.is_a?(Tool) }) + assert_equal(["tool1", "tool2"], all_tools.map(&:name)) + end + + def test_handles_empty_tools_array + response = { "result" => { "tools" => [] } } + tools = Tools.new(response) + + assert_equal([], tools.all) + assert_equal(0, tools.count) + end + + def test_handles_missing_tools_key + response = { "result" => {} } + tools = Tools.new(response) + + assert_equal([], tools.all) + assert_equal(0, tools.count) + end + + def test_handles_missing_result_key + response = {} + tools = Tools.new(response) + + assert_equal([], tools.all) + assert_equal(0, tools.count) + end + + def test_tools_are_initialized_with_correct_payload + response = { + "result" => { + "tools" => [ + { + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { "type" => "object" }, + }, + ], + }, + } + tools = Tools.new(response) + tool = tools.all.first + + assert_equal("test_tool", tool.name) + assert_equal("A test tool", tool.description) + assert_equal({ "type" => "object" }, tool.input_schema) + end + + def test_includes_enumerable + response = { "result" => { "tools" => [] } } + tools = Tools.new(response) + + assert(tools.respond_to?(:map)) + assert(tools.respond_to?(:select)) + assert(tools.respond_to?(:find)) + end + end + end +end diff --git a/test/model_context_protocol/client_test.rb b/test/model_context_protocol/client_test.rb new file mode 100644 index 0000000..01522bf --- /dev/null +++ b/test/model_context_protocol/client_test.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "test_helper" + +module ModelContextProtocol + class ClientTest < Minitest::Test + end +end From 43d4dd014160646f55f64b0d7c00d77d802ca8bb Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Wed, 28 May 2025 16:37:21 -0400 Subject: [PATCH 14/17] exit preview version --- lib/model_context_protocol/shared/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model_context_protocol/shared/version.rb b/lib/model_context_protocol/shared/version.rb index de68e0c..80a6c26 100644 --- a/lib/model_context_protocol/shared/version.rb +++ b/lib/model_context_protocol/shared/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ModelContextProtocol - VERSION = "1.0.0-preview" + VERSION = "1.0.0" end From d8e1caf8b84c5a0b57112167fbe5c1b8107c3366 Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Thu, 29 May 2025 11:22:52 -0400 Subject: [PATCH 15/17] add some client docs --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index 09b5c17..4ce333f 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,47 @@ $ ./stdio_server.rb {"jsonrpc":"2.0","id":"3","result":["ExampleTool"]} ``` +## MCP Client + +The `ModelContextProtocol::Client` module provides client implementations for interacting with MCP servers. Currently, it supports HTTP transport for making JSON-RPC requests to MCP servers. + +### HTTP Client + +The `ModelContextProtocol::Client::Http` class provides a simple HTTP client for interacting with MCP servers: + +```ruby +client = ModelContextProtocol::Client::Http.new(url: "https://api.example.com/mcp") + +# List available tools +tools = client.tools +tools.each do |tool| + puts "Tool: #{tool.name}" + puts "Description: #{tool.description}" + puts "Input Schema: #{tool.input_schema}" +end + +# Call a specific tool +response = client.call_tool( + tool: tools.first, + input: { message: "Hello, world!" } +) +``` + +The HTTP client supports: +- Tool listing via the `tools/list` method +- Tool invocation via the `tools/call` method +- Automatic JSON-RPC 2.0 message formatting +- UUID v7 request ID generation + +### Tool Objects + +The client provides wrapper objects for tools returned by the server: + +- `ModelContextProtocol::Client::Tool` - Represents a single tool with its metadata +- `ModelContextProtocol::Client::Tools` - Collection of tools with enumerable functionality + +These objects provide easy access to tool properties like name, description, and input schema. + ## Configuration The gem can be configured using the `ModelContextProtocol.configure` block: From d1e7a83f71be4645d15dbf739d3084b0b60dad9e Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Thu, 29 May 2025 12:19:47 -0400 Subject: [PATCH 16/17] add more robust error handling --- lib/model_context_protocol/client.rb | 11 + lib/model_context_protocol/client/http.rb | 82 ++++-- .../client/http_test.rb | 258 ++++++++++++++++++ 3 files changed, 330 insertions(+), 21 deletions(-) diff --git a/lib/model_context_protocol/client.rb b/lib/model_context_protocol/client.rb index b321f7a..bd454ca 100644 --- a/lib/model_context_protocol/client.rb +++ b/lib/model_context_protocol/client.rb @@ -7,5 +7,16 @@ module ModelContextProtocol module Client # Can be made an abstract class if we need shared behavior + + class RequestHandlerError < StandardError + attr_reader :error_type, :original_error, :request + + def initialize(message, request, error_type: :internal_error, original_error: nil) + super(message) + @request = request + @error_type = error_type + @original_error = original_error + end + end end end diff --git a/lib/model_context_protocol/client/http.rb b/lib/model_context_protocol/client/http.rb index 714fc5f..f15a221 100644 --- a/lib/model_context_protocol/client/http.rb +++ b/lib/model_context_protocol/client/http.rb @@ -1,9 +1,5 @@ # frozen_string_literal: true -# require "json_rpc_handler" -# require_relative "shared/instrumentation" -# require_relative "shared/methods" - module ModelContextProtocol module Client class Http @@ -17,27 +13,15 @@ def initialize(url:, version: DEFAULT_VERSION) end def tools - response = client.post( - "", - method: "tools/list", - jsonrpc: "2.0", - id: request_id, - mcp: { method: "tools/list", jsonrpc: "2.0", id: request_id }, - ).body + response = make_request(method: "tools/list").body ::ModelContextProtocol::Client::Tools.new(response) end def call_tool(tool:, input:) - response = client.post( - "", - { - jsonrpc: "2.0", - id: request_id, - method: "tools/call", - params: { name: tool.name, arguments: input }, - mcp: { jsonrpc: "2.0", id: request_id, method: "tools/call", params: { name: tool.name, arguments: input } }, - }, + response = make_request( + method: "tools/call", + params: { name: tool.name, arguments: input }, ).body response.dig("result", "content", 0, "text") @@ -45,14 +29,70 @@ def call_tool(tool:, input:) private + # TODO: support auth def client @client ||= Faraday.new(url) do |faraday| faraday.request(:json) faraday.response(:json) - # TODO: error middleware? + faraday.response(:raise_error) end end + def make_request(method:, params: nil) + client.post( + "", + { + jsonrpc: "2.0", + id: request_id, + method:, + params:, + mcp: { jsonrpc: "2.0", id: request_id, method:, params: }.compact, + }.compact, + ) + rescue Faraday::BadRequestError => e + raise RequestHandlerError.new( + "The #{method} request is invalid", + { method:, params: }, + error_type: :bad_request, + original_error: e, + ) + rescue Faraday::UnauthorizedError => e + raise RequestHandlerError.new( + "You are unauthorized to make #{method} requests", + { method:, params: }, + error_type: :unauthorized, + original_error: e, + ) + rescue Faraday::ForbiddenError => e + raise RequestHandlerError.new( + "You are forbidden to make #{method} requests", + { method:, params: }, + error_type: :forbidden, + original_error: e, + ) + rescue Faraday::ResourceNotFound => e + raise RequestHandlerError.new( + "The #{method} request is not found", + { method:, params: }, + error_type: :not_found, + original_error: e, + ) + rescue Faraday::UnprocessableEntityError => e + raise RequestHandlerError.new( + "The #{method} request is unprocessable", + { method:, params: }, + error_type: :unprocessable_entity, + original_error: e, + ) + rescue Faraday::Error => e # Catch-all + raise RequestHandlerError.new( + "Internal error handling #{method} request", + { method:, params: }, + error_type: :internal_error, + original_error: e, + ) + end + def request_id SecureRandom.uuid_v7 end diff --git a/test/model_context_protocol/client/http_test.rb b/test/model_context_protocol/client/http_test.rb index 0fd4413..e84ed8c 100644 --- a/test/model_context_protocol/client/http_test.rb +++ b/test/model_context_protocol/client/http_test.rb @@ -160,6 +160,264 @@ def test_call_tool_handles_empty_response assert_nil(response) end + def test_raises_bad_request_error + tool = Tool.new( + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { + "type" => "object", + "properties" => {}, + }, + ) + input = { "param" => "value" } + + stub_request(:post, url) + .with( + body: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + mcp: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + }, + }, + ) + .to_return(status: 400) + + error = assert_raises(RequestHandlerError) do + client.call_tool(tool: tool, input: input) + end + + assert_equal("The tools/call request is invalid", error.message) + assert_equal(:bad_request, error.error_type) + assert_equal({ method: "tools/call", params: { name: "test_tool", arguments: input } }, error.request) + end + + def test_raises_unauthorized_error + tool = Tool.new( + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { + "type" => "object", + "properties" => {}, + }, + ) + input = { "param" => "value" } + + stub_request(:post, url) + .with( + body: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + mcp: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + }, + }, + ) + .to_return(status: 401) + + error = assert_raises(RequestHandlerError) do + client.call_tool(tool: tool, input: input) + end + + assert_equal("You are unauthorized to make tools/call requests", error.message) + assert_equal(:unauthorized, error.error_type) + assert_equal({ method: "tools/call", params: { name: "test_tool", arguments: input } }, error.request) + end + + def test_raises_forbidden_error + tool = Tool.new( + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { + "type" => "object", + "properties" => {}, + }, + ) + input = { "param" => "value" } + + stub_request(:post, url) + .with( + body: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + mcp: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + }, + }, + ) + .to_return(status: 403) + + error = assert_raises(RequestHandlerError) do + client.call_tool(tool: tool, input: input) + end + + assert_equal("You are forbidden to make tools/call requests", error.message) + assert_equal(:forbidden, error.error_type) + assert_equal({ method: "tools/call", params: { name: "test_tool", arguments: input } }, error.request) + end + + def test_raises_not_found_error + tool = Tool.new( + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { + "type" => "object", + "properties" => {}, + }, + ) + input = { "param" => "value" } + + stub_request(:post, url) + .with( + body: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + mcp: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + }, + }, + ) + .to_return(status: 404) + + error = assert_raises(RequestHandlerError) do + client.call_tool(tool: tool, input: input) + end + + assert_equal("The tools/call request is not found", error.message) + assert_equal(:not_found, error.error_type) + assert_equal({ method: "tools/call", params: { name: "test_tool", arguments: input } }, error.request) + end + + def test_raises_unprocessable_entity_error + tool = Tool.new( + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { + "type" => "object", + "properties" => {}, + }, + ) + input = { "param" => "value" } + + stub_request(:post, url) + .with( + body: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + mcp: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + }, + }, + ) + .to_return(status: 422) + + error = assert_raises(RequestHandlerError) do + client.call_tool(tool: tool, input: input) + end + + assert_equal("The tools/call request is unprocessable", error.message) + assert_equal(:unprocessable_entity, error.error_type) + assert_equal({ method: "tools/call", params: { name: "test_tool", arguments: input } }, error.request) + end + + def test_raises_internal_error + tool = Tool.new( + "name" => "test_tool", + "description" => "A test tool", + "inputSchema" => { + "type" => "object", + "properties" => {}, + }, + ) + input = { "param" => "value" } + + stub_request(:post, url) + .with( + body: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + mcp: { + jsonrpc: "2.0", + id: mock_request_id, + method: "tools/call", + params: { + name: "test_tool", + arguments: input, + }, + }, + }, + ) + .to_return(status: 500) + + error = assert_raises(RequestHandlerError) do + client.call_tool(tool: tool, input: input) + end + + assert_equal("Internal error handling tools/call request", error.message) + assert_equal(:internal_error, error.error_type) + assert_equal({ method: "tools/call", params: { name: "test_tool", arguments: input } }, error.request) + end + private def stub_request(method, url) From e72bcfae8e872b65766e5a2cccbca844ae60d979 Mon Sep 17 00:00:00 2001 From: Joey Cardosi Date: Thu, 29 May 2025 13:53:28 -0400 Subject: [PATCH 17/17] Add basic HTTP client support --- README.md | 18 ++++++++++ lib/model_context_protocol/client/http.rb | 10 ++++-- .../client/http_test.rb | 33 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ce333f..df8390d 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,24 @@ The HTTP client supports: - Tool invocation via the `tools/call` method - Automatic JSON-RPC 2.0 message formatting - UUID v7 request ID generation +- Setting headers for things like authorization + +### HTTP Authorization + +By default, the HTTP client has no authentication, but it supports custom headers for authentication. For example, to use Bearer token authentication: + +```ruby +client = ModelContextProtocol::Client::Http.new( + url: "https://api.example.com/mcp", + headers: { + "Authorization" => "Bearer my_token" + } +) + +client.tools # will make the call using Bearer auth +``` + +You can add any custom headers needed for your authentication scheme. The client will include these headers in all requests. ### Tool Objects diff --git a/lib/model_context_protocol/client/http.rb b/lib/model_context_protocol/client/http.rb index f15a221..05f663d 100644 --- a/lib/model_context_protocol/client/http.rb +++ b/lib/model_context_protocol/client/http.rb @@ -7,9 +7,10 @@ class Http attr_reader :url, :version - def initialize(url:, version: DEFAULT_VERSION) + def initialize(url:, version: DEFAULT_VERSION, headers: {}) @url = url @version = version + @headers = headers end def tools @@ -29,12 +30,17 @@ def call_tool(tool:, input:) private - # TODO: support auth + attr_reader :headers + def client @client ||= Faraday.new(url) do |faraday| faraday.request(:json) faraday.response(:json) faraday.response(:raise_error) + + headers.each do |key, value| + faraday.headers[key] = value + end end end diff --git a/test/model_context_protocol/client/http_test.rb b/test/model_context_protocol/client/http_test.rb index e84ed8c..d5336e1 100644 --- a/test/model_context_protocol/client/http_test.rb +++ b/test/model_context_protocol/client/http_test.rb @@ -19,6 +19,39 @@ def test_initialization_with_custom_version assert_equal(custom_version, client.version) end + def test_headers_are_added_to_the_request + headers = { "Authorization" => "Bearer token" } + client = Http.new(url:, headers:) + client.stubs(:request_id).returns(mock_request_id) + + stub_request(:post, url) + .with( + headers: { + "Authorization" => "Bearer token", + "Content-Type" => "application/json", + }, + body: { + method: "tools/list", + jsonrpc: "2.0", + id: mock_request_id, + mcp: { + method: "tools/list", + jsonrpc: "2.0", + id: mock_request_id, + }, + }, + ) + .to_return( + status: 200, + headers: { "Content-Type" => "application/json" }, + body: { result: { tools: [] } }.to_json, + ) + + # The test passes if the request is made with the correct headers + # If headers are wrong, the stub_request won't match and will raise + client.tools + end + def test_tools_returns_tools_instance stub_request(:post, url) .with(