@mastra/mcp 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +34 -0
- package/dist/_tsup-dts-rollup.d.cts +332 -40
- package/dist/_tsup-dts-rollup.d.ts +332 -40
- package/dist/index.cjs +599 -45
- package/dist/index.d.cts +14 -9
- package/dist/index.d.ts +14 -9
- package/dist/index.js +604 -50
- package/package.json +4 -3
- package/src/__fixtures__/fire-crawl-complex-schema.ts +1 -1
- package/src/__fixtures__/server-weather.ts +1 -1
- package/src/__fixtures__/weather.ts +122 -190
- package/src/{client.test.ts → client/client.test.ts} +27 -0
- package/src/{client.ts → client/client.ts} +58 -5
- package/src/{configuration.test.ts → client/configuration.test.ts} +133 -21
- package/src/{configuration.ts → client/configuration.ts} +64 -48
- package/src/client/resourceActions.ts +121 -0
- package/src/index.ts +4 -4
- package/src/server/resourceActions.ts +62 -0
- package/src/{server-logging.test.ts → server/server-logging.test.ts} +9 -7
- package/src/server/server.test.ts +1126 -0
- package/src/{server.ts → server/server.ts} +444 -16
- package/src/server.test.ts +0 -467
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import type * as http from 'node:http';
|
|
3
3
|
import type { InternalCoreTool } from '@mastra/core';
|
|
4
|
-
import { makeCoreTool } from '@mastra/core';
|
|
4
|
+
import { createTool, makeCoreTool } from '@mastra/core';
|
|
5
5
|
import type { ToolsInput } from '@mastra/core/agent';
|
|
6
|
+
import { Agent } from '@mastra/core/agent';
|
|
6
7
|
import { MCPServerBase } from '@mastra/core/mcp';
|
|
7
8
|
import type {
|
|
8
9
|
MCPServerConfig,
|
|
@@ -13,17 +14,46 @@ import type {
|
|
|
13
14
|
MCPServerSSEOptions,
|
|
14
15
|
} from '@mastra/core/mcp';
|
|
15
16
|
import { RuntimeContext } from '@mastra/core/runtime-context';
|
|
17
|
+
import type { Workflow } from '@mastra/core/workflows';
|
|
16
18
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
17
19
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
18
20
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
21
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
20
22
|
import type { StreamableHTTPServerTransportOptions } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
21
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
CallToolRequestSchema,
|
|
25
|
+
ListToolsRequestSchema,
|
|
26
|
+
ListResourcesRequestSchema,
|
|
27
|
+
ReadResourceRequestSchema,
|
|
28
|
+
ListResourceTemplatesRequestSchema,
|
|
29
|
+
SubscribeRequestSchema,
|
|
30
|
+
UnsubscribeRequestSchema,
|
|
31
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
32
|
+
import type {
|
|
33
|
+
ResourceContents,
|
|
34
|
+
Resource,
|
|
35
|
+
ResourceTemplate,
|
|
36
|
+
ServerCapabilities,
|
|
37
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
22
38
|
import type { SSEStreamingApi } from 'hono/streaming';
|
|
23
39
|
import { streamSSE } from 'hono/streaming';
|
|
24
40
|
import { SSETransport } from 'hono-mcp-server-sse-transport';
|
|
25
41
|
import { z } from 'zod';
|
|
26
|
-
|
|
42
|
+
import { ServerResourceActions } from './resourceActions';
|
|
43
|
+
|
|
44
|
+
export type MCPServerResourceContentCallback = ({
|
|
45
|
+
uri,
|
|
46
|
+
}: {
|
|
47
|
+
uri: string;
|
|
48
|
+
}) => Promise<MCPServerResourceContent | MCPServerResourceContent[]>;
|
|
49
|
+
export type MCPServerResourceContent = { text?: string } | { blob?: string };
|
|
50
|
+
export type MCPServerResources = {
|
|
51
|
+
listResources: () => Promise<Resource[]>;
|
|
52
|
+
getResourceContent: MCPServerResourceContentCallback;
|
|
53
|
+
resourceTemplates?: () => Promise<ResourceTemplate[]>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type { Resource, ResourceTemplate };
|
|
27
57
|
export class MCPServer extends MCPServerBase {
|
|
28
58
|
private server: Server;
|
|
29
59
|
private stdioTransport?: StdioServerTransport;
|
|
@@ -32,6 +62,17 @@ export class MCPServer extends MCPServerBase {
|
|
|
32
62
|
private streamableHTTPTransport?: StreamableHTTPServerTransport;
|
|
33
63
|
private listToolsHandlerIsRegistered: boolean = false;
|
|
34
64
|
private callToolHandlerIsRegistered: boolean = false;
|
|
65
|
+
private listResourcesHandlerIsRegistered: boolean = false;
|
|
66
|
+
private readResourceHandlerIsRegistered: boolean = false;
|
|
67
|
+
private listResourceTemplatesHandlerIsRegistered: boolean = false;
|
|
68
|
+
private subscribeResourceHandlerIsRegistered: boolean = false;
|
|
69
|
+
private unsubscribeResourceHandlerIsRegistered: boolean = false;
|
|
70
|
+
|
|
71
|
+
private definedResources?: Resource[];
|
|
72
|
+
private definedResourceTemplates?: ResourceTemplate[];
|
|
73
|
+
private resourceOptions?: MCPServerResources;
|
|
74
|
+
private subscriptions: Set<string> = new Set();
|
|
75
|
+
public readonly resources: ServerResourceActions;
|
|
35
76
|
|
|
36
77
|
/**
|
|
37
78
|
* Get the current stdio transport.
|
|
@@ -65,30 +106,216 @@ export class MCPServer extends MCPServerBase {
|
|
|
65
106
|
* Construct a new MCPServer instance.
|
|
66
107
|
* @param opts - Configuration options for the server, including registry metadata.
|
|
67
108
|
*/
|
|
68
|
-
constructor(opts: MCPServerConfig) {
|
|
109
|
+
constructor(opts: MCPServerConfig & { resources?: MCPServerResources }) {
|
|
69
110
|
super(opts);
|
|
111
|
+
this.resourceOptions = opts.resources;
|
|
70
112
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
const capabilities: ServerCapabilities = {
|
|
114
|
+
tools: {},
|
|
115
|
+
logging: { enabled: true },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (opts.resources) {
|
|
119
|
+
capabilities.resources = { subscribe: true, listChanged: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.server = new Server({ name: this.name, version: this.version }, { capabilities });
|
|
75
123
|
|
|
76
124
|
this.logger.info(
|
|
77
|
-
`Initialized MCPServer '${this.name}' v${this.version} (ID: ${this.id}) with tools: ${Object.keys(this.convertedTools).join(', ')}`,
|
|
125
|
+
`Initialized MCPServer '${this.name}' v${this.version} (ID: ${this.id}) with tools: ${Object.keys(this.convertedTools).join(', ')} and resources. Capabilities: ${JSON.stringify(capabilities)}`,
|
|
78
126
|
);
|
|
79
127
|
|
|
80
128
|
this.sseHonoTransports = new Map();
|
|
81
129
|
this.registerListToolsHandler();
|
|
82
130
|
this.registerCallToolHandler();
|
|
131
|
+
if (opts.resources) {
|
|
132
|
+
this.registerListResourcesHandler();
|
|
133
|
+
this.registerReadResourceHandler({ getResourcesCallback: opts.resources.getResourceContent });
|
|
134
|
+
this.registerSubscribeResourceHandler();
|
|
135
|
+
this.registerUnsubscribeResourceHandler();
|
|
136
|
+
|
|
137
|
+
if (opts.resources.resourceTemplates) {
|
|
138
|
+
this.registerListResourceTemplatesHandler();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.resources = new ServerResourceActions({
|
|
142
|
+
getSubscriptions: () => this.subscriptions,
|
|
143
|
+
getLogger: () => this.logger,
|
|
144
|
+
getSdkServer: () => this.server,
|
|
145
|
+
clearDefinedResources: () => {
|
|
146
|
+
this.definedResources = undefined;
|
|
147
|
+
},
|
|
148
|
+
clearDefinedResourceTemplates: () => {
|
|
149
|
+
this.definedResourceTemplates = undefined;
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private convertAgentsToTools(
|
|
155
|
+
agentsConfig?: Record<string, Agent>,
|
|
156
|
+
definedConvertedTools?: Record<string, ConvertedTool>,
|
|
157
|
+
): Record<string, ConvertedTool> {
|
|
158
|
+
const agentTools: Record<string, ConvertedTool> = {};
|
|
159
|
+
if (!agentsConfig) {
|
|
160
|
+
return agentTools;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const agentKey in agentsConfig) {
|
|
164
|
+
const agent = agentsConfig[agentKey];
|
|
165
|
+
if (!agent || !(agent instanceof Agent)) {
|
|
166
|
+
this.logger.warn(`Agent instance for '${agentKey}' is invalid or missing a generate function. Skipping.`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const agentDescription = agent.getDescription();
|
|
171
|
+
|
|
172
|
+
if (!agentDescription) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Agent '${agent.name}' (key: '${agentKey}') must have a non-empty description to be used in an MCPServer.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const agentToolName = `ask_${agentKey}`;
|
|
179
|
+
if (definedConvertedTools?.[agentToolName] || agentTools[agentToolName]) {
|
|
180
|
+
this.logger.warn(
|
|
181
|
+
`Tool with name '${agentToolName}' already exists. Agent '${agentKey}' will not be added as a duplicate tool.`,
|
|
182
|
+
);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const agentToolDefinition = createTool({
|
|
187
|
+
id: agentToolName,
|
|
188
|
+
description: `Ask agent '${agent.name}' a question. Agent description: ${agentDescription}`,
|
|
189
|
+
inputSchema: z.object({
|
|
190
|
+
message: z.string().describe('The question or input for the agent.'),
|
|
191
|
+
}),
|
|
192
|
+
execute: async ({ context, runtimeContext }) => {
|
|
193
|
+
this.logger.debug(
|
|
194
|
+
`Executing agent tool '${agentToolName}' for agent '${agent.name}' with message: "${context.message}"`,
|
|
195
|
+
);
|
|
196
|
+
try {
|
|
197
|
+
const response = await agent.generate(context.message, { runtimeContext });
|
|
198
|
+
return response;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this.logger.error(`Error executing agent tool '${agentToolName}' for agent '${agent.name}':`, error);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const options = {
|
|
207
|
+
name: agentToolName,
|
|
208
|
+
logger: this.logger,
|
|
209
|
+
mastra: this.mastra,
|
|
210
|
+
runtimeContext: new RuntimeContext(),
|
|
211
|
+
description: agentToolDefinition.description,
|
|
212
|
+
};
|
|
213
|
+
const coreTool = makeCoreTool(agentToolDefinition, options) as InternalCoreTool;
|
|
214
|
+
|
|
215
|
+
agentTools[agentToolName] = {
|
|
216
|
+
name: agentToolName,
|
|
217
|
+
description: coreTool.description,
|
|
218
|
+
parameters: coreTool.parameters,
|
|
219
|
+
execute: coreTool.execute!,
|
|
220
|
+
};
|
|
221
|
+
this.logger.info(`Registered agent '${agent.name}' (key: '${agentKey}') as tool: '${agentToolName}'`);
|
|
222
|
+
}
|
|
223
|
+
return agentTools;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private convertWorkflowsToTools(
|
|
227
|
+
workflowsConfig?: Record<string, Workflow>,
|
|
228
|
+
definedConvertedTools?: Record<string, ConvertedTool>,
|
|
229
|
+
): Record<string, ConvertedTool> {
|
|
230
|
+
const workflowTools: Record<string, ConvertedTool> = {};
|
|
231
|
+
if (!workflowsConfig) {
|
|
232
|
+
return workflowTools;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const workflowKey in workflowsConfig) {
|
|
236
|
+
const workflow = workflowsConfig[workflowKey];
|
|
237
|
+
if (!workflow || typeof workflow.createRun !== 'function') {
|
|
238
|
+
this.logger.warn(
|
|
239
|
+
`Workflow instance for '${workflowKey}' is invalid or missing a createRun function. Skipping.`,
|
|
240
|
+
);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const workflowDescription = workflow.description;
|
|
245
|
+
if (!workflowDescription) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Workflow '${workflow.id}' (key: '${workflowKey}') must have a non-empty description to be used in an MCPServer.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const workflowToolName = `run_${workflowKey}`;
|
|
252
|
+
if (definedConvertedTools?.[workflowToolName] || workflowTools[workflowToolName]) {
|
|
253
|
+
this.logger.warn(
|
|
254
|
+
`Tool with name '${workflowToolName}' already exists. Workflow '${workflowKey}' will not be added as a duplicate tool.`,
|
|
255
|
+
);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const workflowToolDefinition = createTool({
|
|
260
|
+
id: workflowToolName,
|
|
261
|
+
description: `Run workflow '${workflowKey}'. Workflow description: ${workflowDescription}`,
|
|
262
|
+
inputSchema: workflow.inputSchema,
|
|
263
|
+
execute: async ({ context, runtimeContext }) => {
|
|
264
|
+
this.logger.debug(
|
|
265
|
+
`Executing workflow tool '${workflowToolName}' for workflow '${workflow.id}' with input:`,
|
|
266
|
+
context,
|
|
267
|
+
);
|
|
268
|
+
try {
|
|
269
|
+
const run = workflow.createRun({ runId: runtimeContext?.get('runId') });
|
|
270
|
+
|
|
271
|
+
const response = await run.start({ inputData: context, runtimeContext });
|
|
272
|
+
|
|
273
|
+
return response;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this.logger.error(
|
|
276
|
+
`Error executing workflow tool '${workflowToolName}' for workflow '${workflow.id}':`,
|
|
277
|
+
error,
|
|
278
|
+
);
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const options = {
|
|
285
|
+
name: workflowToolName,
|
|
286
|
+
logger: this.logger,
|
|
287
|
+
mastra: this.mastra,
|
|
288
|
+
runtimeContext: new RuntimeContext(),
|
|
289
|
+
description: workflowToolDefinition.description,
|
|
290
|
+
};
|
|
291
|
+
const coreTool = makeCoreTool(workflowToolDefinition, options) as InternalCoreTool;
|
|
292
|
+
|
|
293
|
+
workflowTools[workflowToolName] = {
|
|
294
|
+
name: workflowToolName,
|
|
295
|
+
description: coreTool.description,
|
|
296
|
+
parameters: coreTool.parameters,
|
|
297
|
+
execute: coreTool.execute!,
|
|
298
|
+
};
|
|
299
|
+
this.logger.info(`Registered workflow '${workflow.id}' (key: '${workflowKey}') as tool: '${workflowToolName}'`);
|
|
300
|
+
}
|
|
301
|
+
return workflowTools;
|
|
83
302
|
}
|
|
84
303
|
|
|
85
304
|
/**
|
|
86
305
|
* Convert and validate all provided tools, logging registration status.
|
|
306
|
+
* Also converts agents and workflows into tools.
|
|
87
307
|
* @param tools Tool definitions
|
|
308
|
+
* @param agentsConfig Agent definitions to be converted to tools, expected from MCPServerConfig
|
|
309
|
+
* @param workflowsConfig Workflow definitions to be converted to tools, expected from MCPServerConfig
|
|
88
310
|
* @returns Converted tools registry
|
|
89
311
|
*/
|
|
90
|
-
convertTools(
|
|
91
|
-
|
|
312
|
+
convertTools(
|
|
313
|
+
tools: ToolsInput,
|
|
314
|
+
agentsConfig?: Record<string, Agent>,
|
|
315
|
+
workflowsConfig?: Record<string, Workflow>,
|
|
316
|
+
): Record<string, ConvertedTool> {
|
|
317
|
+
const definedConvertedTools: Record<string, ConvertedTool> = {};
|
|
318
|
+
|
|
92
319
|
for (const toolName of Object.keys(tools)) {
|
|
93
320
|
const toolInstance = tools[toolName];
|
|
94
321
|
if (!toolInstance) {
|
|
@@ -111,16 +338,30 @@ export class MCPServer extends MCPServerBase {
|
|
|
111
338
|
|
|
112
339
|
const coreTool = makeCoreTool(toolInstance, options) as InternalCoreTool;
|
|
113
340
|
|
|
114
|
-
|
|
341
|
+
definedConvertedTools[toolName] = {
|
|
115
342
|
name: toolName,
|
|
116
343
|
description: coreTool.description,
|
|
117
344
|
parameters: coreTool.parameters,
|
|
118
345
|
execute: coreTool.execute!,
|
|
119
346
|
};
|
|
120
|
-
this.logger.info(`Registered tool: '${toolName}'
|
|
347
|
+
this.logger.info(`Registered explicit tool: '${toolName}'`);
|
|
121
348
|
}
|
|
122
|
-
this.logger.info(`Total tools registered: ${Object.keys(
|
|
123
|
-
|
|
349
|
+
this.logger.info(`Total defined tools registered: ${Object.keys(definedConvertedTools).length}`);
|
|
350
|
+
|
|
351
|
+
const agentDerivedTools = this.convertAgentsToTools(agentsConfig, definedConvertedTools);
|
|
352
|
+
const workflowDerivedTools = this.convertWorkflowsToTools(workflowsConfig, definedConvertedTools);
|
|
353
|
+
|
|
354
|
+
const allConvertedTools = { ...definedConvertedTools, ...agentDerivedTools, ...workflowDerivedTools };
|
|
355
|
+
|
|
356
|
+
const finalToolCount = Object.keys(allConvertedTools).length;
|
|
357
|
+
const definedCount = Object.keys(definedConvertedTools).length;
|
|
358
|
+
const fromAgentsCount = Object.keys(agentDerivedTools).length;
|
|
359
|
+
const fromWorkflowsCount = Object.keys(workflowDerivedTools).length;
|
|
360
|
+
this.logger.info(
|
|
361
|
+
`${finalToolCount} total tools registered (${definedCount} defined + ${fromAgentsCount} agents + ${fromWorkflowsCount} workflows)`,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
return allConvertedTools;
|
|
124
365
|
}
|
|
125
366
|
|
|
126
367
|
/**
|
|
@@ -222,6 +463,187 @@ export class MCPServer extends MCPServerBase {
|
|
|
222
463
|
});
|
|
223
464
|
}
|
|
224
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Register the ListResources handler for listing all available resources.
|
|
468
|
+
*/
|
|
469
|
+
private registerListResourcesHandler() {
|
|
470
|
+
if (this.listResourcesHandlerIsRegistered) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
this.listResourcesHandlerIsRegistered = true;
|
|
474
|
+
const capturedResourceOptions = this.resourceOptions; // Capture for TS narrowing
|
|
475
|
+
|
|
476
|
+
if (!capturedResourceOptions?.listResources) {
|
|
477
|
+
this.logger.warn('ListResources capability not supported by server configuration.');
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
482
|
+
this.logger.debug('Handling ListResources request');
|
|
483
|
+
if (this.definedResources) {
|
|
484
|
+
return { resources: this.definedResources };
|
|
485
|
+
} else {
|
|
486
|
+
try {
|
|
487
|
+
const resources = await capturedResourceOptions.listResources();
|
|
488
|
+
// Cache the resources
|
|
489
|
+
this.definedResources = resources;
|
|
490
|
+
this.logger.debug(`Fetched and cached ${this.definedResources.length} resources.`);
|
|
491
|
+
return { resources: this.definedResources };
|
|
492
|
+
} catch (error) {
|
|
493
|
+
this.logger.error('Error fetching resources via listResources():', { error });
|
|
494
|
+
// Re-throw to let the MCP Server SDK handle formatting the error response
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Register the ReadResource handler for reading a resource by URI.
|
|
503
|
+
*/
|
|
504
|
+
private registerReadResourceHandler({
|
|
505
|
+
getResourcesCallback,
|
|
506
|
+
}: {
|
|
507
|
+
getResourcesCallback: MCPServerResourceContentCallback;
|
|
508
|
+
}) {
|
|
509
|
+
if (this.readResourceHandlerIsRegistered) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
this.readResourceHandlerIsRegistered = true;
|
|
513
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async request => {
|
|
514
|
+
const startTime = Date.now();
|
|
515
|
+
const uri = request.params.uri;
|
|
516
|
+
this.logger.debug(`Handling ReadResource request for URI: ${uri}`);
|
|
517
|
+
|
|
518
|
+
if (!this.definedResources) {
|
|
519
|
+
const resources = await this.resourceOptions?.listResources?.();
|
|
520
|
+
if (!resources) throw new Error('Failed to load resources');
|
|
521
|
+
this.definedResources = resources;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const resource = this.definedResources?.find(r => r.uri === uri);
|
|
525
|
+
|
|
526
|
+
if (!resource) {
|
|
527
|
+
this.logger.warn(`ReadResource: Unknown resource URI '${uri}' requested.`);
|
|
528
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
const resourcesOrResourceContent = await getResourcesCallback({ uri });
|
|
533
|
+
const resourcesContent = Array.isArray(resourcesOrResourceContent)
|
|
534
|
+
? resourcesOrResourceContent
|
|
535
|
+
: [resourcesOrResourceContent];
|
|
536
|
+
const contents: ResourceContents[] = resourcesContent.map(resourceContent => {
|
|
537
|
+
const contentItem: ResourceContents = {
|
|
538
|
+
uri: resource.uri,
|
|
539
|
+
mimeType: resource.mimeType,
|
|
540
|
+
};
|
|
541
|
+
if ('text' in resourceContent) {
|
|
542
|
+
contentItem.text = resourceContent.text;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if ('blob' in resourceContent) {
|
|
546
|
+
contentItem.blob = resourceContent.blob;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return contentItem;
|
|
550
|
+
});
|
|
551
|
+
const duration = Date.now() - startTime;
|
|
552
|
+
this.logger.info(`Resource '${uri}' read successfully in ${duration}ms.`);
|
|
553
|
+
return {
|
|
554
|
+
contents,
|
|
555
|
+
};
|
|
556
|
+
} catch (error) {
|
|
557
|
+
const duration = Date.now() - startTime;
|
|
558
|
+
this.logger.error(`Failed to get content for resource URI '${uri}' in ${duration}ms`, { error });
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Register the ListResourceTemplates handler.
|
|
566
|
+
*/
|
|
567
|
+
private registerListResourceTemplatesHandler() {
|
|
568
|
+
if (this.listResourceTemplatesHandlerIsRegistered) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// If this method is called, this.resourceOptions and this.resourceOptions.resourceTemplates should exist
|
|
573
|
+
// due to the constructor logic checking opts.resources.resourceTemplates.
|
|
574
|
+
if (!this.resourceOptions || typeof this.resourceOptions.resourceTemplates !== 'function') {
|
|
575
|
+
this.logger.warn(
|
|
576
|
+
'ListResourceTemplates handler called, but resourceTemplates function is not available on resourceOptions or not a function.',
|
|
577
|
+
);
|
|
578
|
+
// Register a handler that returns empty templates if not properly configured.
|
|
579
|
+
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
580
|
+
this.logger.debug('Handling ListResourceTemplates request (no templates configured or resourceOptions issue)');
|
|
581
|
+
return { resourceTemplates: [] };
|
|
582
|
+
});
|
|
583
|
+
this.listResourceTemplatesHandlerIsRegistered = true;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Typescript can now infer resourceTemplatesFn is a function.
|
|
588
|
+
const resourceTemplatesFn = this.resourceOptions.resourceTemplates;
|
|
589
|
+
|
|
590
|
+
this.listResourceTemplatesHandlerIsRegistered = true;
|
|
591
|
+
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
592
|
+
this.logger.debug('Handling ListResourceTemplates request');
|
|
593
|
+
if (this.definedResourceTemplates) {
|
|
594
|
+
return { resourceTemplates: this.definedResourceTemplates };
|
|
595
|
+
} else {
|
|
596
|
+
try {
|
|
597
|
+
const templates = await resourceTemplatesFn(); // Safe to call now
|
|
598
|
+
this.definedResourceTemplates = templates;
|
|
599
|
+
this.logger.debug(`Fetched and cached ${this.definedResourceTemplates.length} resource templates.`);
|
|
600
|
+
return { resourceTemplates: this.definedResourceTemplates };
|
|
601
|
+
} catch (error) {
|
|
602
|
+
this.logger.error('Error fetching resource templates via resourceTemplates():', { error });
|
|
603
|
+
// Re-throw to let the MCP Server SDK handle formatting the error response
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Register the SubscribeResource handler.
|
|
612
|
+
*/
|
|
613
|
+
private registerSubscribeResourceHandler() {
|
|
614
|
+
if (this.subscribeResourceHandlerIsRegistered) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (!SubscribeRequestSchema) {
|
|
618
|
+
this.logger.warn('SubscribeRequestSchema not available, cannot register SubscribeResource handler.');
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.subscribeResourceHandlerIsRegistered = true;
|
|
622
|
+
this.server.setRequestHandler(SubscribeRequestSchema as any, async (request: { params: { uri: string } }) => {
|
|
623
|
+
const uri = request.params.uri;
|
|
624
|
+
this.logger.info(`Received resources/subscribe request for URI: ${uri}`);
|
|
625
|
+
this.subscriptions.add(uri);
|
|
626
|
+
return {};
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Register the UnsubscribeResource handler.
|
|
632
|
+
*/
|
|
633
|
+
private registerUnsubscribeResourceHandler() {
|
|
634
|
+
if (this.unsubscribeResourceHandlerIsRegistered) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
this.unsubscribeResourceHandlerIsRegistered = true;
|
|
638
|
+
|
|
639
|
+
this.server.setRequestHandler(UnsubscribeRequestSchema as any, async (request: { params: { uri: string } }) => {
|
|
640
|
+
const uri = request.params.uri;
|
|
641
|
+
this.logger.info(`Received resources/unsubscribe request for URI: ${uri}`);
|
|
642
|
+
this.subscriptions.delete(uri);
|
|
643
|
+
return {};
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
225
647
|
/**
|
|
226
648
|
* Start the MCP server using stdio transport (for Windsurf integration).
|
|
227
649
|
*/
|
|
@@ -413,6 +835,12 @@ export class MCPServer extends MCPServerBase {
|
|
|
413
835
|
async close() {
|
|
414
836
|
this.callToolHandlerIsRegistered = false;
|
|
415
837
|
this.listToolsHandlerIsRegistered = false;
|
|
838
|
+
this.listResourcesHandlerIsRegistered = false;
|
|
839
|
+
this.readResourceHandlerIsRegistered = false;
|
|
840
|
+
this.listResourceTemplatesHandlerIsRegistered = false;
|
|
841
|
+
this.subscribeResourceHandlerIsRegistered = false;
|
|
842
|
+
this.unsubscribeResourceHandlerIsRegistered = false;
|
|
843
|
+
|
|
416
844
|
try {
|
|
417
845
|
if (this.stdioTransport) {
|
|
418
846
|
await this.stdioTransport.close?.();
|