-
Notifications
You must be signed in to change notification settings - Fork 7
Description
Summary
The MCP specification includes resources/subscribe and resources/unsubscribe methods for clients to receive notifications when resources change. These are currently not implemented.
Additionally, resources/read uses exact URI matching only. When a resource is registered with a uriSchema for query parameters, reading resource://base?id=123 fails because the Map lookup is literal.
Current Behavior
// Client calls resources/subscribe
// Server returns: METHOD_NOT_FOUND (-32601)For URI with query params:
app.mcpAddResource({
uri: 'aip://findings',
name: 'Findings',
uriSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }
}, async (uri, params) => {
// params.id should contain the query parameter value
const finding = await db.getFinding(params.id);
return { contents: [{ uri, text: JSON.stringify(finding), mimeType: 'application/json' }] };
});
// Reading 'aip://findings?id=abc123' fails - exact match onlyProposed Solution
1. Custom Subscription Handlers
Rather than building subscription tracking into the library, provide hooks for applications to implement their own storage:
// Application creates its own subscription store (memory, Redis, etc.)
const subscriptionStore = createSubscriptionStore();
app.mcpSetResourcesSubscribeHandler(async (params, context) => {
await subscriptionStore.subscribe(context.sessionId, params.uri);
return {};
});
app.mcpSetResourcesUnsubscribeHandler(async (params, context) => {
await subscriptionStore.unsubscribe(context.sessionId, params.uri);
return {};
});
// When resource changes, notify via existing mcpSendToSession
const subscribers = await subscriptionStore.getSubscribers(changedUri);
for (const sessionId of subscribers) {
await app.mcpSendToSession(sessionId, {
jsonrpc: '2.0',
method: 'notifications/resources/updated',
params: { uri: changedUri }
});
}2. Query Parameter URI Matching
When exact match fails in resources/read:
let resource = resources.get(uri);
// If not found and URI has query params, try base URI
if (!resource && uri.includes('?')) {
const baseUri = uri.split('?')[0];
const baseResource = resources.get(baseUri);
// Only use if resource has uriSchema (expects query params)
if (baseResource?.definition?.uriSchema) {
resource = baseResource;
}
}3. Handler Types
interface ResourceHandlerContext {
sessionId?: string;
request: FastifyRequest;
reply: FastifyReply;
authContext?: AuthorizationContext;
}
type ResourceSubscribeHandler = (
params: { uri: string },
context: ResourceHandlerContext
) => Promise<Record<string, unknown>>;
type ResourceUnsubscribeHandler = (
params: { uri: string },
context: ResourceHandlerContext
) => Promise<Record<string, unknown>>;Files to Modify
src/handlers.ts- Add subscription handlers, query param fallbacksrc/decorators/meta.ts- Add setter decoratorssrc/types.ts- Add handler types, Fastify declaration mergingsrc/index.ts- Create resourceHandlers object, pass throughsrc/routes/mcp.ts- Include resourceHandlers in dependencies
Design Decisions
-
Custom handlers, not built-in tracking: Applications manage their own storage (memory, Redis, etc.). This follows the library's pattern of delegating application-specific logic.
-
METHOD_NOT_FOUND when not configured: Clear signal that feature isn't enabled, rather than silent success.
-
uriSchema as query param indicator: Only falls back to base URI if resource explicitly expects query params via
uriSchema. -
Dependency injection:
resourceHandlersobject passed through plugin chain, avoiding module-level state.
Backwards Compatibility
- Existing
mcpAddResourceregistrations unchanged - Default
resources/readbehavior unchanged for exact matches - Subscribe/unsubscribe return errors (not silent success) when not configured
- No changes to existing decorators or APIs
I'm happy to submit a PR with these changes. I have a working implementation.