@@ -59,7 +59,8 @@ declare global {
5959 }
6060}
6161
62- // ── MCP Streamable HTTP client (minimal, browser-side) ───────────────
62+ import { Client } from "@modelcontextprotocol/sdk/client/index.js" ;
63+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" ;
6364
6465interface McpTool {
6566 name : string ;
@@ -68,242 +69,118 @@ interface McpTool {
6869 annotations ?: { readOnlyHint ?: boolean } ;
6970}
7071
71- interface McpToolsListResult {
72- tools : McpTool [ ] ;
73- }
74-
7572interface McpToolCallResult {
7673 content : Array < { type : string ; text ?: string ; data ?: string } > ;
7774 isError ?: boolean ;
7875}
7976
80- interface JsonRpcRequest {
81- jsonrpc : "2.0" ;
82- id ?: string | number ;
83- method : string ;
84- params ?: Record < string , unknown > ;
85- }
86-
87- interface JsonRpcResponse {
88- jsonrpc : "2.0" ;
89- id ?: string | number | null ;
90- result ?: unknown ;
91- error ?: { code : number ; message : string } ;
92- }
93-
94- /** Minimal MCP Streamable HTTP client for browser use. */
9577class McpHttpClient {
96- private _url : string ;
97- private _sessionId : string | null = null ;
98- private _nextId = 1 ;
99- private _abortController : AbortController | null = null ;
100- private _headers : Record < string , string > ;
101- private _getHeaders ?: ( ) =>
102- | Promise < Record < string , string > >
103- | Record < string , string > ;
78+ private _client : Client ;
79+ private _transport : StreamableHTTPClientTransport ;
80+ private _onToolsChanged ?: ( ) => void ;
10481
10582 constructor (
10683 url : string ,
10784 headers ?: Record < string , string > ,
10885 getHeaders ?: ( ) => Promise < Record < string , string > > | Record < string , string >
10986 ) {
110- // Resolve relative URLs against current origin
111- this . _url = new URL ( url , globalThis . location ?. origin ) . href ;
112- this . _headers = headers ?? { } ;
113- this . _getHeaders = getHeaders ;
114- }
87+ const resolvedUrl = new URL ( url , globalThis . location ?. origin ) ;
11588
116- /** Send a JSON-RPC request and parse the SSE response. */
117- private async _send (
118- method : string ,
119- params ?: Record < string , unknown > ,
120- id ?: number
121- ) : Promise < JsonRpcResponse | null > {
122- const body : JsonRpcRequest = {
123- jsonrpc : "2.0" ,
124- method,
125- ...( id != null ? { id } : { } ) ,
126- ...( params ? { params } : { } )
89+ const transportOptions : ConstructorParameters <
90+ typeof StreamableHTTPClientTransport
91+ > [ 1 ] = {
92+ requestInit : { headers : headers ?? { } }
12793 } ;
12894
129- const dynamic = this . _getHeaders ? await this . _getHeaders ( ) : { } ;
130- const headers : Record < string , string > = {
131- ...this . _headers ,
132- ...dynamic ,
133- "Content-Type" : "application/json" ,
134- Accept : "application/json, text/event-stream"
135- } ;
136-
137- if ( this . _sessionId ) {
138- headers [ "mcp-session-id" ] = this . _sessionId ;
139- }
140-
141- const res = await fetch ( this . _url , {
142- method : "POST" ,
143- headers,
144- body : JSON . stringify ( body )
145- } ) ;
146-
147- // Capture session ID from response headers
148- const sid = res . headers . get ( "mcp-session-id" ) ;
149- if ( sid ) {
150- this . _sessionId = sid ;
151- }
152-
153- // Notifications (no id)
154- if ( id == null ) {
155- return null ;
156- }
157-
158- const contentType = res . headers . get ( "content-type" ) ?? "" ;
159-
160- // Direct JSON response
161- if ( contentType . includes ( "application/json" ) ) {
162- return ( await res . json ( ) ) as JsonRpcResponse ;
163- }
164-
165- // SSE response — parse the first "message" event
166- if ( contentType . includes ( "text/event-stream" ) ) {
167- return this . _parseSSE ( res ) ;
95+ if ( getHeaders ) {
96+ transportOptions . fetch = async ( input , init ) => {
97+ const dynamic = await getHeaders ( ) ;
98+ return globalThis . fetch ( input , {
99+ ...init ,
100+ headers : {
101+ ...( init ?. headers as Record < string , string > ) ,
102+ ...dynamic
103+ }
104+ } ) ;
105+ } ;
168106 }
169107
170- throw new Error ( `Unexpected content-type from MCP server: ${ contentType } ` ) ;
171- }
172-
173- /** Parse a Server-Sent Events response and return the first message. */
174- private async _parseSSE ( res : Response ) : Promise < JsonRpcResponse > {
175- const text = await res . text ( ) ;
176- const lines = text . split ( "\n" ) ;
177- for ( const line of lines ) {
178- if ( line . startsWith ( "data: " ) ) {
179- const data = line . slice ( 6 ) . trim ( ) ;
180- if ( data ) {
181- return JSON . parse ( data ) as JsonRpcResponse ;
182- }
183- }
184- }
185- throw new Error ( "No data event found in SSE response" ) ;
186- }
108+ this . _transport = new StreamableHTTPClientTransport (
109+ resolvedUrl ,
110+ transportOptions
111+ ) ;
187112
188- /** Initialize the MCP session. */
189- async initialize ( ) : Promise < void > {
190- const id = this . _nextId ++ ;
191- const res = await this . _send (
192- "initialize" ,
113+ this . _client = new Client (
114+ { name : "webmcp-adapter" , version : "0.1.0" } ,
193115 {
194- protocolVersion : "2024-11-05" ,
195116 capabilities : { } ,
196- clientInfo : {
197- name : "webmcp-adapter" ,
198- version : "0.1.0"
117+ listChanged : {
118+ tools : {
119+ onChanged : ( ) => {
120+ this . _onToolsChanged ?.( ) ;
121+ }
122+ }
199123 }
200- } ,
201- id
124+ }
202125 ) ;
126+ }
203127
204- if ( res ?. error ) {
205- throw new Error ( `MCP initialize failed: ${ res . error . message } ` ) ;
206- }
207-
208- // Send initialized notification
209- await this . _send ( "notifications/initialized" , { } ) ;
128+ async initialize ( ) : Promise < void > {
129+ await this . _client . connect ( this . _transport ) ;
210130 }
211131
212- /** List all tools from the MCP server. */
213132 async listTools ( ) : Promise < McpTool [ ] > {
214- const id = this . _nextId ++ ;
215- const res = await this . _send ( "tools/list" , { } , id ) ;
216-
217- if ( res ?. error ) {
218- throw new Error ( `MCP tools/list failed: ${ res . error . message } ` ) ;
219- }
220-
221- const result = res ?. result as McpToolsListResult | undefined ;
222- return result ?. tools ?? [ ] ;
133+ const allTools : McpTool [ ] = [ ] ;
134+ let cursor : string | undefined ;
135+ do {
136+ const result = await this . _client . listTools (
137+ cursor ? { cursor } : undefined
138+ ) ;
139+ for ( const t of result . tools ) {
140+ allTools . push ( {
141+ name : t . name ,
142+ description : t . description ,
143+ inputSchema : t . inputSchema as Record < string , unknown > | undefined ,
144+ annotations : t . annotations
145+ ? { readOnlyHint : t . annotations . readOnlyHint }
146+ : undefined
147+ } ) ;
148+ }
149+ cursor = result . nextCursor ;
150+ } while ( cursor ) ;
151+ return allTools ;
223152 }
224153
225- /** Call a tool on the MCP server. */
226154 async callTool (
227155 name : string ,
228156 args : Record < string , unknown >
229157 ) : Promise < McpToolCallResult > {
230- const id = this . _nextId ++ ;
231- const res = await this . _send ( "tools/call" , { name, arguments : args } , id ) ;
232-
233- if ( res ?. error ) {
234- throw new Error ( `MCP tools/call failed: ${ res . error . message } ` ) ;
158+ const result = await this . _client . callTool ( { name, arguments : args } ) ;
159+ if ( "content" in result ) {
160+ return {
161+ content : (
162+ result . content as Array < {
163+ type : string ;
164+ text ?: string ;
165+ data ?: string ;
166+ } >
167+ ) . map ( ( c ) => ( {
168+ type : c . type ,
169+ text : "text" in c ? ( c . text as string ) : undefined ,
170+ data : "data" in c ? ( c . data as string ) : undefined
171+ } ) ) ,
172+ isError : "isError" in result ? ( result . isError as boolean ) : false
173+ } ;
235174 }
236-
237- return ( res ?. result as McpToolCallResult ) ?? { content : [ ] } ;
175+ return { content : [ ] , isError : false } ;
238176 }
239177
240- /** Open an SSE stream for server notifications (tools/list_changed). */
241178 listenForChanges ( onToolsChanged : ( ) => void ) : void {
242- if ( ! this . _sessionId ) return ;
243-
244- this . _abortController = new AbortController ( ) ;
245-
246- Promise . resolve ( this . _getHeaders ? this . _getHeaders ( ) : { } )
247- . then ( ( dynamic ) => {
248- const headers : Record < string , string > = {
249- ...this . _headers ,
250- ...dynamic ,
251- Accept : "text/event-stream"
252- } ;
253- if ( this . _sessionId ) {
254- headers [ "mcp-session-id" ] = this . _sessionId ;
255- }
256- return fetch ( this . _url , {
257- method : "GET" ,
258- headers,
259- signal : this . _abortController ?. signal
260- } ) ;
261- } )
262- . then ( async ( res ) => {
263- if ( ! res . body ) return ;
264- const reader = res . body . getReader ( ) ;
265- const decoder = new TextDecoder ( ) ;
266- let buffer = "" ;
267-
268- while ( true ) {
269- const { done, value } = await reader . read ( ) ;
270- if ( done ) break ;
271-
272- buffer += decoder . decode ( value , { stream : true } ) ;
273- const lines = buffer . split ( "\n" ) ;
274- buffer = lines . pop ( ) ?? "" ;
275-
276- for ( const line of lines ) {
277- if ( line . startsWith ( "data: " ) ) {
278- const data = line . slice ( 6 ) . trim ( ) ;
279- if ( ! data ) continue ;
280- try {
281- const msg = JSON . parse ( data ) as JsonRpcResponse ;
282- if (
283- "method" in msg &&
284- ( msg as unknown as { method : string } ) . method ===
285- "notifications/tools/list_changed"
286- ) {
287- onToolsChanged ( ) ;
288- }
289- } catch {
290- // Ignore non-JSON SSE data
291- }
292- }
293- }
294- }
295- } )
296- . catch ( ( err : unknown ) => {
297- // AbortError is expected on dispose
298- if ( err instanceof Error && err . name === "AbortError" ) return ;
299- console . warn ( "[webmcp-adapter] SSE listener error:" , err ) ;
300- } ) ;
179+ this . _onToolsChanged = onToolsChanged ;
301180 }
302181
303- /** Close the SSE listener. */
304182 close ( ) : void {
305- this . _abortController ?. abort ( ) ;
306- this . _abortController = null ;
183+ this . _client . close ( ) . catch ( ( ) => { } ) ;
307184 }
308185}
309186
@@ -424,8 +301,17 @@ export async function registerWebMcp(
424301 throw new Error ( errorText || "Tool execution failed" ) ;
425302 }
426303
427- // Return the text content as the result
428- return result . content . map ( ( c ) => c . text ?? "" ) . join ( "\n" ) ;
304+ const parts : string [ ] = [ ] ;
305+ for ( const c of result . content ) {
306+ if ( c . type === "text" && c . text ) {
307+ parts . push ( c . text ) ;
308+ } else if ( c . type === "image" && c . data ) {
309+ parts . push ( `data:image;base64,${ c . data } ` ) ;
310+ } else if ( c . data ) {
311+ parts . push ( c . data ) ;
312+ }
313+ }
314+ return parts . join ( "\n" ) ;
429315 }
430316 } ;
431317
0 commit comments