Skip to content

Commit 9e33919

Browse files
committed
feat(satellite): add MCP resource proxying and preserve _meta for MCP Apps support
Implement full MCP resource proxying through the satellite's hierarchical and instance routers. Previously, MCP servers that expose resources (like Excalidraw's interactive UI) didn't work through DeployStack because the satellite returned empty resource lists and didn't handle resource reads. Additionally, preserve the _meta field through the entire tool and resource cache chain so MCP Apps work via DeployStack. MCP Apps are discovered by clients checking tools/list for _meta.ui.resourceUri metadata — without this, apps appeared empty when connecting through the proxy. Resource proxying: - Add UnifiedResourceDiscoveryManager for caching resource metadata - Add McpResourceExecutor for proxying resources/read on demand - Discover resources alongside tools during server connection - Register native SDK resource handlers in both routers - Add list_mcp_resources and read_mcp_resource meta-tools - Use pipe-separated URI namespacing for hierarchical router - Update legacy SSE route to proxy real resource data _meta preservation: - Add _meta field to all tool cache interfaces across the chain - Add _meta field to resource and template cache interfaces - Propagate _meta through tool search service results - Include _meta in all tools/list and resources/list responses - Rewrite _meta.ui.resourceUri to namespaced format in discover_mcp_tools
1 parent 9fe1dee commit 9e33919

11 files changed

Lines changed: 1204 additions & 32 deletions

services/satellite/src/core/instance-router.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
33
import {
44
ListToolsRequestSchema,
5-
CallToolRequestSchema
5+
CallToolRequestSchema,
6+
ListResourcesRequestSchema,
7+
ReadResourceRequestSchema,
8+
ListResourceTemplatesRequestSchema
69
} from '@modelcontextprotocol/sdk/types.js';
710
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
811
import { FastifyInstance, FastifyRequest, FastifyReply, FastifyBaseLogger } from 'fastify';
912
import { createHash } from 'crypto';
1013
import { McpToolExecutor } from '../lib/mcp-tool-executor';
14+
import { McpResourceExecutor } from '../lib/mcp-resource-executor';
1115
import { McpSessionManager } from '../lib/mcp-session-manager';
1216
import { UnifiedToolDiscoveryManager } from '../services/unified-tool-discovery-manager';
1317
import { DynamicConfigManager } from '../services/dynamic-config-manager';
@@ -38,6 +42,7 @@ interface InstanceContext {
3842
export class InstanceRouter {
3943
private logger: FastifyBaseLogger;
4044
private toolExecutor: McpToolExecutor;
45+
private resourceExecutor?: McpResourceExecutor;
4146
private sessionManager: McpSessionManager;
4247
private configManager: DynamicConfigManager;
4348
private toolDiscoveryManager: UnifiedToolDiscoveryManager;
@@ -47,6 +52,7 @@ export class InstanceRouter {
4752
constructor(deps: {
4853
logger: FastifyBaseLogger;
4954
toolExecutor: McpToolExecutor;
55+
resourceExecutor?: McpResourceExecutor;
5056
sessionManager: McpSessionManager;
5157
configManager: DynamicConfigManager;
5258
toolDiscoveryManager: UnifiedToolDiscoveryManager;
@@ -55,6 +61,7 @@ export class InstanceRouter {
5561
}) {
5662
this.logger = deps.logger.child({ component: 'InstanceRouter' });
5763
this.toolExecutor = deps.toolExecutor;
64+
this.resourceExecutor = deps.resourceExecutor;
5865
this.sessionManager = deps.sessionManager;
5966
this.configManager = deps.configManager;
6067
this.toolDiscoveryManager = deps.toolDiscoveryManager;
@@ -111,6 +118,7 @@ export class InstanceRouter {
111118
name: tool.originalName, // Original tool name (NOT namespaced)
112119
description: tool.description,
113120
inputSchema: tool.inputSchema,
121+
...(tool._meta ? { _meta: tool._meta } : {}),
114122
})),
115123
};
116124
});
@@ -222,6 +230,74 @@ export class InstanceRouter {
222230
}
223231
});
224232

233+
// Register resources/list handler - return resources from THIS instance using original URIs
234+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
235+
const allResources = this.toolDiscoveryManager.getResourcesByServer(processId);
236+
237+
this.logger.debug({
238+
operation: 'instance_resources_list',
239+
process_id: processId,
240+
resource_count: allResources.length
241+
}, `Listing ${allResources.length} resources for instance ${processId}`);
242+
243+
return {
244+
resources: allResources.map(r => ({
245+
uri: r.originalUri, // Original URI (no namespacing for instance router)
246+
name: r.name,
247+
description: r.description,
248+
mimeType: r.mimeType,
249+
annotations: r.annotations,
250+
...(r._meta ? { _meta: r._meta } : {})
251+
}))
252+
};
253+
});
254+
255+
// Register resources/read handler - proxy to instance
256+
server.setRequestHandler(ReadResourceRequestSchema, async (request: any) => {
257+
const uri = request.params?.uri;
258+
if (!uri) {
259+
throw new Error('Missing required parameter: uri');
260+
}
261+
262+
if (!this.resourceExecutor) {
263+
throw new Error('Resource executor not available');
264+
}
265+
266+
this.logger.info({
267+
operation: 'instance_resource_read',
268+
process_id: processId,
269+
uri
270+
}, `Reading resource ${uri} from instance ${processId}`);
271+
272+
// Determine transport type from config
273+
const serverConfig = this.configManager.getMcpServerConfig(processId);
274+
const transport = (serverConfig?.transport_type || serverConfig?.type || 'stdio') as 'stdio' | 'http' | 'sse';
275+
276+
return await this.resourceExecutor.readResourceDirect(processId, uri, transport);
277+
});
278+
279+
// Register resources/templates/list handler
280+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
281+
const allTemplates = this.toolDiscoveryManager.getResourceTemplatesByServer(processId);
282+
283+
this.logger.debug({
284+
operation: 'instance_resource_templates_list',
285+
process_id: processId,
286+
template_count: allTemplates.length
287+
}, `Listing ${allTemplates.length} resource templates for instance ${processId}`);
288+
289+
return {
290+
resourceTemplates: allTemplates.map(t => ({
291+
uriTemplate: t.originalUriTemplate, // Original URI template (no namespacing)
292+
name: t.name,
293+
description: t.description,
294+
mimeType: t.mimeType,
295+
annotations: t.annotations,
296+
...(t._meta ? { _meta: t._meta } : {})
297+
}))
298+
};
299+
});
300+
225301
this.logger.info({
226302
operation: 'instance_mcp_server_setup',
227303
process_id: processId

0 commit comments

Comments
 (0)