1
1
import type { Tool } from '@langchain/core/tools' ;
2
2
import { Server } from '@modelcontextprotocol/sdk/server/index.js' ;
3
3
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js' ;
4
- import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' ;
4
+ import type {
5
+ JSONRPCMessage ,
6
+ ServerRequest ,
7
+ ServerNotification ,
8
+ } from '@modelcontextprotocol/sdk/types.js' ;
5
9
import {
6
10
JSONRPCMessageSchema ,
7
11
ListToolsRequestSchema ,
@@ -33,6 +37,20 @@ function wasToolCall(body: string) {
33
37
}
34
38
}
35
39
40
+ /**
41
+ * Extracts the request ID from a JSONRPC message
42
+ * Returns undefined if the message doesn't have an ID or can't be parsed
43
+ */
44
+ function getRequestId ( body : string ) : string | undefined {
45
+ try {
46
+ const message : unknown = JSON . parse ( body ) ;
47
+ const parsedMessage : JSONRPCMessage = JSONRPCMessageSchema . parse ( message ) ;
48
+ return 'id' in parsedMessage ? String ( parsedMessage . id ) : undefined ;
49
+ } catch {
50
+ return undefined ;
51
+ }
52
+ }
53
+
36
54
export class McpServer {
37
55
servers : { [ sessionId : string ] : Server } = { } ;
38
56
@@ -42,7 +60,7 @@ export class McpServer {
42
60
43
61
private tools : { [ sessionId : string ] : Tool [ ] } = { } ;
44
62
45
- private resolveFunctions : { [ sessionId : string ] : CallableFunction } = { } ;
63
+ private resolveFunctions : { [ callId : string ] : CallableFunction } = { } ;
46
64
47
65
constructor ( logger : Logger ) {
48
66
this . logger = logger ;
@@ -59,7 +77,6 @@ export class McpServer {
59
77
resp . on ( 'close' , async ( ) => {
60
78
this . logger . debug ( `Deleting transport for ${ sessionId } ` ) ;
61
79
delete this . tools [ sessionId ] ;
62
- delete this . resolveFunctions [ sessionId ] ;
63
80
delete this . transports [ sessionId ] ;
64
81
delete this . servers [ sessionId ] ;
65
82
} ) ;
@@ -75,16 +92,25 @@ export class McpServer {
75
92
async handlePostMessage ( req : express . Request , resp : CompressionResponse , connectedTools : Tool [ ] ) {
76
93
const sessionId = req . query . sessionId as string ;
77
94
const transport = this . transports [ sessionId ] ;
78
- this . tools [ sessionId ] = connectedTools ;
79
95
if ( transport ) {
80
96
// We need to add a promise here because the `handlePostMessage` will send something to the
81
97
// MCP Server, that will run in a different context. This means that the return will happen
82
98
// almost immediately, and will lead to marking the sub-node as "running" in the final execution
83
- await new Promise ( async ( resolve ) => {
84
- this . resolveFunctions [ sessionId ] = resolve ;
85
- await transport . handlePostMessage ( req , resp , req . rawBody . toString ( ) ) ;
86
- } ) ;
87
- delete this . resolveFunctions [ sessionId ] ;
99
+ const bodyString = req . rawBody . toString ( ) ;
100
+ const messageId = getRequestId ( bodyString ) ;
101
+
102
+ // Use session & message ID if available, otherwise fall back to sessionId
103
+ const callId = messageId ? `${ sessionId } _${ messageId } ` : sessionId ;
104
+ this . tools [ sessionId ] = connectedTools ;
105
+
106
+ try {
107
+ await new Promise ( async ( resolve ) => {
108
+ this . resolveFunctions [ callId ] = resolve ;
109
+ await transport . handlePostMessage ( req , resp , bodyString ) ;
110
+ } ) ;
111
+ } finally {
112
+ delete this . resolveFunctions [ callId ] ;
113
+ }
88
114
} else {
89
115
this . logger . warn ( `No transport found for session ${ sessionId } ` ) ;
90
116
resp . status ( 401 ) . send ( 'No transport found for sessionId' ) ;
@@ -94,8 +120,6 @@ export class McpServer {
94
120
resp . flush ( ) ;
95
121
}
96
122
97
- delete this . tools [ sessionId ] ; // Clean up to avoid keeping all tools in memory
98
-
99
123
return wasToolCall ( req . rawBody . toString ( ) ) ;
100
124
}
101
125
@@ -110,57 +134,68 @@ export class McpServer {
110
134
} ,
111
135
) ;
112
136
113
- server . setRequestHandler ( ListToolsRequestSchema , async ( _ , extra : RequestHandlerExtra ) => {
114
- if ( ! extra . sessionId ) {
115
- throw new OperationalError ( 'Require a sessionId for the listing of tools' ) ;
116
- }
117
-
118
- return {
119
- tools : this . tools [ extra . sessionId ] . map ( ( tool ) => {
120
- return {
121
- name : tool . name ,
122
- description : tool . description ,
123
- // Allow additional properties on tool call input
124
- inputSchema : zodToJsonSchema ( tool . schema , { removeAdditionalStrategy : 'strict' } ) ,
125
- } ;
126
- } ) ,
127
- } ;
128
- } ) ;
129
-
130
- server . setRequestHandler ( CallToolRequestSchema , async ( request , extra : RequestHandlerExtra ) => {
131
- if ( ! request . params ?. name || ! request . params ?. arguments ) {
132
- throw new OperationalError ( 'Require a name and arguments for the tool call' ) ;
133
- }
134
- if ( ! extra . sessionId ) {
135
- throw new OperationalError ( 'Require a sessionId for the tool call' ) ;
136
- }
137
-
138
- const requestedTool : Tool | undefined = this . tools [ extra . sessionId ] . find (
139
- ( tool ) => tool . name === request . params . name ,
140
- ) ;
141
- if ( ! requestedTool ) {
142
- throw new OperationalError ( 'Tool not found' ) ;
143
- }
137
+ server . setRequestHandler (
138
+ ListToolsRequestSchema ,
139
+ async ( _ , extra : RequestHandlerExtra < ServerRequest , ServerNotification > ) => {
140
+ if ( ! extra . sessionId ) {
141
+ throw new OperationalError ( 'Require a sessionId for the listing of tools' ) ;
142
+ }
144
143
145
- try {
146
- const result = await requestedTool . invoke ( request . params . arguments ) ;
144
+ return {
145
+ tools : this . tools [ extra . sessionId ] . map ( ( tool ) => {
146
+ return {
147
+ name : tool . name ,
148
+ description : tool . description ,
149
+ // Allow additional properties on tool call input
150
+ inputSchema : zodToJsonSchema ( tool . schema , { removeAdditionalStrategy : 'strict' } ) ,
151
+ } ;
152
+ } ) ,
153
+ } ;
154
+ } ,
155
+ ) ;
147
156
148
- this . resolveFunctions [ extra . sessionId ] ( ) ;
157
+ server . setRequestHandler (
158
+ CallToolRequestSchema ,
159
+ async ( request , extra : RequestHandlerExtra < ServerRequest , ServerNotification > ) => {
160
+ if ( ! request . params ?. name || ! request . params ?. arguments ) {
161
+ throw new OperationalError ( 'Require a name and arguments for the tool call' ) ;
162
+ }
163
+ if ( ! extra . sessionId ) {
164
+ throw new OperationalError ( 'Require a sessionId for the tool call' ) ;
165
+ }
149
166
150
- this . logger . debug ( `Got request for ${ requestedTool . name } , and executed it.` ) ;
167
+ const callId = extra . requestId ? ` ${ extra . sessionId } _ ${ extra . requestId } ` : extra . sessionId ;
151
168
152
- if ( typeof result === 'object' ) {
153
- return { content : [ { type : 'text' , text : JSON . stringify ( result ) } ] } ;
169
+ const requestedTool : Tool | undefined = this . tools [ extra . sessionId ] . find (
170
+ ( tool ) => tool . name === request . params . name ,
171
+ ) ;
172
+ if ( ! requestedTool ) {
173
+ throw new OperationalError ( 'Tool not found' ) ;
154
174
}
155
- if ( typeof result === 'string' ) {
156
- return { content : [ { type : 'text' , text : result } ] } ;
175
+
176
+ try {
177
+ const result = await requestedTool . invoke ( request . params . arguments ) ;
178
+ if ( this . resolveFunctions [ callId ] ) {
179
+ this . resolveFunctions [ callId ] ( ) ;
180
+ } else {
181
+ this . logger . warn ( `No resolve function found for ${ callId } ` ) ;
182
+ }
183
+
184
+ this . logger . debug ( `Got request for ${ requestedTool . name } , and executed it.` ) ;
185
+
186
+ if ( typeof result === 'object' ) {
187
+ return { content : [ { type : 'text' , text : JSON . stringify ( result ) } ] } ;
188
+ }
189
+ if ( typeof result === 'string' ) {
190
+ return { content : [ { type : 'text' , text : result } ] } ;
191
+ }
192
+ return { content : [ { type : 'text' , text : String ( result ) } ] } ;
193
+ } catch ( error ) {
194
+ this . logger . error ( `Error while executing Tool ${ requestedTool . name } : ${ error } ` ) ;
195
+ return { isError : true , content : [ { type : 'text' , text : `Error: ${ error . message } ` } ] } ;
157
196
}
158
- return { content : [ { type : 'text' , text : String ( result ) } ] } ;
159
- } catch ( error ) {
160
- this . logger . error ( `Error while executing Tool ${ requestedTool . name } : ${ error } ` ) ;
161
- return { isError : true , content : [ { type : 'text' , text : `Error: ${ error . message } ` } ] } ;
162
- }
163
- } ) ;
197
+ } ,
198
+ ) ;
164
199
165
200
server . onclose = ( ) => {
166
201
this . logger . debug ( 'Closing MCP Server' ) ;
0 commit comments