toolhome 0.5.0
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/LICENSE +21 -0
- package/README.md +295 -0
- package/README.zh.md +281 -0
- package/dist/server/app.d.ts +16 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +166 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/cli/main.d.ts +3 -0
- package/dist/server/cli/main.d.ts.map +1 -0
- package/dist/server/cli/main.js +587 -0
- package/dist/server/cli/main.js.map +1 -0
- package/dist/server/cli-plane/allow-list.d.ts +18 -0
- package/dist/server/cli-plane/allow-list.d.ts.map +1 -0
- package/dist/server/cli-plane/allow-list.js +28 -0
- package/dist/server/cli-plane/allow-list.js.map +1 -0
- package/dist/server/cli-plane/cli-service.d.ts +41 -0
- package/dist/server/cli-plane/cli-service.d.ts.map +1 -0
- package/dist/server/cli-plane/cli-service.js +282 -0
- package/dist/server/cli-plane/cli-service.js.map +1 -0
- package/dist/server/cli-plane/frames.d.ts +26 -0
- package/dist/server/cli-plane/frames.d.ts.map +1 -0
- package/dist/server/cli-plane/frames.js +12 -0
- package/dist/server/cli-plane/frames.js.map +1 -0
- package/dist/server/cli-plane/models.d.ts +174 -0
- package/dist/server/cli-plane/models.d.ts.map +1 -0
- package/dist/server/cli-plane/models.js +169 -0
- package/dist/server/cli-plane/models.js.map +1 -0
- package/dist/server/cli-plane/routes.d.ts +32 -0
- package/dist/server/cli-plane/routes.d.ts.map +1 -0
- package/dist/server/cli-plane/routes.js +94 -0
- package/dist/server/cli-plane/routes.js.map +1 -0
- package/dist/server/cli-plane/runner.d.ts +41 -0
- package/dist/server/cli-plane/runner.d.ts.map +1 -0
- package/dist/server/cli-plane/runner.js +163 -0
- package/dist/server/cli-plane/runner.js.map +1 -0
- package/dist/server/cli-plane/status.d.ts +12 -0
- package/dist/server/cli-plane/status.d.ts.map +1 -0
- package/dist/server/cli-plane/status.js +24 -0
- package/dist/server/cli-plane/status.js.map +1 -0
- package/dist/server/config/harness-import.d.ts +44 -0
- package/dist/server/config/harness-import.d.ts.map +1 -0
- package/dist/server/config/harness-import.js +166 -0
- package/dist/server/config/harness-import.js.map +1 -0
- package/dist/server/config.d.ts +20 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +80 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/control/client.d.ts +24 -0
- package/dist/server/control/client.d.ts.map +1 -0
- package/dist/server/control/client.js +115 -0
- package/dist/server/control/client.js.map +1 -0
- package/dist/server/control/control-api.d.ts +39 -0
- package/dist/server/control/control-api.d.ts.map +1 -0
- package/dist/server/control/control-api.js +291 -0
- package/dist/server/control/control-api.js.map +1 -0
- package/dist/server/control/control-service.d.ts +713 -0
- package/dist/server/control/control-service.d.ts.map +1 -0
- package/dist/server/control/control-service.js +693 -0
- package/dist/server/control/control-service.js.map +1 -0
- package/dist/server/control/openapi.d.ts +2 -0
- package/dist/server/control/openapi.d.ts.map +1 -0
- package/dist/server/control/openapi.js +370 -0
- package/dist/server/control/openapi.js.map +1 -0
- package/dist/server/data-plane/data-plane.d.ts +20 -0
- package/dist/server/data-plane/data-plane.d.ts.map +1 -0
- package/dist/server/data-plane/data-plane.js +335 -0
- package/dist/server/data-plane/data-plane.js.map +1 -0
- package/dist/server/data-plane/gateway-server.d.ts +13 -0
- package/dist/server/data-plane/gateway-server.d.ts.map +1 -0
- package/dist/server/data-plane/gateway-server.js +796 -0
- package/dist/server/data-plane/gateway-server.js.map +1 -0
- package/dist/server/data-plane/legacy-session.d.ts +18 -0
- package/dist/server/data-plane/legacy-session.d.ts.map +1 -0
- package/dist/server/data-plane/legacy-session.js +137 -0
- package/dist/server/data-plane/legacy-session.js.map +1 -0
- package/dist/server/data-plane/projection.d.ts +18 -0
- package/dist/server/data-plane/projection.d.ts.map +1 -0
- package/dist/server/data-plane/projection.js +44 -0
- package/dist/server/data-plane/projection.js.map +1 -0
- package/dist/server/data-plane/registry.d.ts +31 -0
- package/dist/server/data-plane/registry.d.ts.map +1 -0
- package/dist/server/data-plane/registry.js +129 -0
- package/dist/server/data-plane/registry.js.map +1 -0
- package/dist/server/data-plane/task-extension.d.ts +7 -0
- package/dist/server/data-plane/task-extension.d.ts.map +1 -0
- package/dist/server/data-plane/task-extension.js +33 -0
- package/dist/server/data-plane/task-extension.js.map +1 -0
- package/dist/server/data-plane/virtualization.d.ts +35 -0
- package/dist/server/data-plane/virtualization.d.ts.map +1 -0
- package/dist/server/data-plane/virtualization.js +228 -0
- package/dist/server/data-plane/virtualization.js.map +1 -0
- package/dist/server/domain/errors.d.ts +9 -0
- package/dist/server/domain/errors.d.ts.map +1 -0
- package/dist/server/domain/errors.js +19 -0
- package/dist/server/domain/errors.js.map +1 -0
- package/dist/server/domain/models.d.ts +720 -0
- package/dist/server/domain/models.d.ts.map +1 -0
- package/dist/server/domain/models.js +327 -0
- package/dist/server/domain/models.js.map +1 -0
- package/dist/server/main.d.ts +2 -0
- package/dist/server/main.d.ts.map +1 -0
- package/dist/server/main.js +37 -0
- package/dist/server/main.js.map +1 -0
- package/dist/server/manage/management-mcp.d.ts +27 -0
- package/dist/server/manage/management-mcp.d.ts.map +1 -0
- package/dist/server/manage/management-mcp.js +431 -0
- package/dist/server/manage/management-mcp.js.map +1 -0
- package/dist/server/market/catalog.d.ts +111 -0
- package/dist/server/market/catalog.d.ts.map +1 -0
- package/dist/server/market/catalog.js +784 -0
- package/dist/server/market/catalog.js.map +1 -0
- package/dist/server/market/installers.d.ts +64 -0
- package/dist/server/market/installers.d.ts.map +1 -0
- package/dist/server/market/installers.js +181 -0
- package/dist/server/market/installers.js.map +1 -0
- package/dist/server/market/market-service.d.ts +146 -0
- package/dist/server/market/market-service.d.ts.map +1 -0
- package/dist/server/market/market-service.js +930 -0
- package/dist/server/market/market-service.js.map +1 -0
- package/dist/server/observability/call-recorder.d.ts +22 -0
- package/dist/server/observability/call-recorder.d.ts.map +1 -0
- package/dist/server/observability/call-recorder.js +99 -0
- package/dist/server/observability/call-recorder.js.map +1 -0
- package/dist/server/observability/logger.d.ts +16 -0
- package/dist/server/observability/logger.d.ts.map +1 -0
- package/dist/server/observability/logger.js +25 -0
- package/dist/server/observability/logger.js.map +1 -0
- package/dist/server/security/api-keys.d.ts +15 -0
- package/dist/server/security/api-keys.d.ts.map +1 -0
- package/dist/server/security/api-keys.js +35 -0
- package/dist/server/security/api-keys.js.map +1 -0
- package/dist/server/security/auth-service.d.ts +26 -0
- package/dist/server/security/auth-service.d.ts.map +1 -0
- package/dist/server/security/auth-service.js +76 -0
- package/dist/server/security/auth-service.js.map +1 -0
- package/dist/server/security/control-session.d.ts +9 -0
- package/dist/server/security/control-session.d.ts.map +1 -0
- package/dist/server/security/control-session.js +51 -0
- package/dist/server/security/control-session.js.map +1 -0
- package/dist/server/security/credential-materializer.d.ts +20 -0
- package/dist/server/security/credential-materializer.d.ts.map +1 -0
- package/dist/server/security/credential-materializer.js +82 -0
- package/dist/server/security/credential-materializer.js.map +1 -0
- package/dist/server/security/cursor-codec.d.ts +13 -0
- package/dist/server/security/cursor-codec.d.ts.map +1 -0
- package/dist/server/security/cursor-codec.js +43 -0
- package/dist/server/security/cursor-codec.js.map +1 -0
- package/dist/server/security/oauth-routes.d.ts +21 -0
- package/dist/server/security/oauth-routes.d.ts.map +1 -0
- package/dist/server/security/oauth-routes.js +100 -0
- package/dist/server/security/oauth-routes.js.map +1 -0
- package/dist/server/security/oauth-server.d.ts +16 -0
- package/dist/server/security/oauth-server.d.ts.map +1 -0
- package/dist/server/security/oauth-server.js +544 -0
- package/dist/server/security/oauth-server.js.map +1 -0
- package/dist/server/security/secret-box.d.ts +7 -0
- package/dist/server/security/secret-box.d.ts.map +1 -0
- package/dist/server/security/secret-box.js +27 -0
- package/dist/server/security/secret-box.js.map +1 -0
- package/dist/server/security/secure-action.d.ts +21 -0
- package/dist/server/security/secure-action.d.ts.map +1 -0
- package/dist/server/security/secure-action.js +69 -0
- package/dist/server/security/secure-action.js.map +1 -0
- package/dist/server/storage/sqlite-store.d.ts +70 -0
- package/dist/server/storage/sqlite-store.d.ts.map +1 -0
- package/dist/server/storage/sqlite-store.js +1522 -0
- package/dist/server/storage/sqlite-store.js.map +1 -0
- package/dist/server/storage/store.d.ts +82 -0
- package/dist/server/storage/store.d.ts.map +1 -0
- package/dist/server/storage/store.js +2 -0
- package/dist/server/storage/store.js.map +1 -0
- package/dist/server/upstream/adapter.d.ts +52 -0
- package/dist/server/upstream/adapter.d.ts.map +1 -0
- package/dist/server/upstream/adapter.js +807 -0
- package/dist/server/upstream/adapter.js.map +1 -0
- package/dist/server/upstream/credential-resolver.d.ts +14 -0
- package/dist/server/upstream/credential-resolver.d.ts.map +1 -0
- package/dist/server/upstream/credential-resolver.js +56 -0
- package/dist/server/upstream/credential-resolver.js.map +1 -0
- package/dist/server/upstream/extension-transport.d.ts +17 -0
- package/dist/server/upstream/extension-transport.d.ts.map +1 -0
- package/dist/server/upstream/extension-transport.js +207 -0
- package/dist/server/upstream/extension-transport.js.map +1 -0
- package/dist/server/upstream/manager.d.ts +23 -0
- package/dist/server/upstream/manager.d.ts.map +1 -0
- package/dist/server/upstream/manager.js +324 -0
- package/dist/server/upstream/manager.js.map +1 -0
- package/dist/server/upstream/oauth-provider.d.ts +38 -0
- package/dist/server/upstream/oauth-provider.d.ts.map +1 -0
- package/dist/server/upstream/oauth-provider.js +225 -0
- package/dist/server/upstream/oauth-provider.js.map +1 -0
- package/dist/server/upstream/oauth-refresh.d.ts +65 -0
- package/dist/server/upstream/oauth-refresh.d.ts.map +1 -0
- package/dist/server/upstream/oauth-refresh.js +240 -0
- package/dist/server/upstream/oauth-refresh.js.map +1 -0
- package/dist/server/upstream/oauth-routes.d.ts +17 -0
- package/dist/server/upstream/oauth-routes.d.ts.map +1 -0
- package/dist/server/upstream/oauth-routes.js +87 -0
- package/dist/server/upstream/oauth-routes.js.map +1 -0
- package/dist/server/upstream/oauth-service.d.ts +143 -0
- package/dist/server/upstream/oauth-service.d.ts.map +1 -0
- package/dist/server/upstream/oauth-service.js +217 -0
- package/dist/server/upstream/oauth-service.js.map +1 -0
- package/dist/server/upstream/stable-json.d.ts +3 -0
- package/dist/server/upstream/stable-json.d.ts.map +1 -0
- package/dist/server/upstream/stable-json.js +17 -0
- package/dist/server/upstream/stable-json.js.map +1 -0
- package/package.json +69 -0
- package/skills/toolhome/SKILL.md +238 -0
- package/skills/toolhome/references/cli-reference.md +242 -0
- package/skills/toolhome/references/deployment.md +113 -0
- package/skills/toolhome/references/market-guide.md +128 -0
- package/skills/toolhome/references/oauth-guide.md +96 -0
- package/skills/toolhome/references/troubleshooting.md +133 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
import { CallToolRequestParamsSchema, CallToolResultSchema, ClientCapabilitiesSchema, CompleteResultSchema, EmptyResultSchema, GetPromptResultSchema, ListPromptsResultSchema, ListResourceTemplatesResultSchema, ListResourcesResultSchema, ListToolsResultSchema, ReadResourceResultSchema, ResultSchema, } from '@modelcontextprotocol/core';
|
|
2
|
+
import { CLIENT_CAPABILITIES_META_KEY, ProtocolError, ProtocolErrorCode, SdkError, SdkErrorCode, Server, createRequestStateCodec, isInputRequiredResult, } from '@modelcontextprotocol/server';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { AppError } from '../domain/errors.js';
|
|
6
|
+
import { fingerprint } from '../upstream/stable-json.js';
|
|
7
|
+
import { CapabilityRegistry } from './registry.js';
|
|
8
|
+
import { canonicalTaskMethod } from './task-extension.js';
|
|
9
|
+
import { ToolProjectionService } from './projection.js';
|
|
10
|
+
import { aggregateName, aggregateExtensionMethod, expandVirtualResourceTemplate, parseVirtualResourceUri, parseVirtualResourceTemplate, parseVirtualTaskId, rewriteAggregateContent, rewriteAggregateTask, rewriteAggregateTool, restoreAggregateContent, splitAggregateName, splitAggregateExtensionMethod, virtualResourceTemplate, virtualResourceUri, } from './virtualization.js';
|
|
11
|
+
const paramsSchema = z.record(z.string(), z.unknown());
|
|
12
|
+
const inputRequiredResultSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
resultType: z.literal('input_required'),
|
|
15
|
+
inputRequests: z.record(z.string(), z.unknown()).optional(),
|
|
16
|
+
requestState: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
.passthrough();
|
|
19
|
+
const extensionTaskResultSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
resultType: z.literal('task'),
|
|
22
|
+
taskId: z.string().min(1),
|
|
23
|
+
})
|
|
24
|
+
.passthrough();
|
|
25
|
+
const legacyTaskResultSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
task: z.object({ taskId: z.string().min(1) }).passthrough(),
|
|
28
|
+
})
|
|
29
|
+
.passthrough();
|
|
30
|
+
const gatewayCallResultSchema = z.union([
|
|
31
|
+
CallToolResultSchema,
|
|
32
|
+
inputRequiredResultSchema,
|
|
33
|
+
extensionTaskResultSchema,
|
|
34
|
+
legacyTaskResultSchema,
|
|
35
|
+
]);
|
|
36
|
+
const taskParamsSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
taskId: z.string().min(1),
|
|
39
|
+
})
|
|
40
|
+
.passthrough();
|
|
41
|
+
const pageSize = 100;
|
|
42
|
+
export class GatewayServerFactory {
|
|
43
|
+
#stateCodec;
|
|
44
|
+
#aggregateServers = new WeakSet();
|
|
45
|
+
#registry;
|
|
46
|
+
#upstreams;
|
|
47
|
+
#cursors;
|
|
48
|
+
#projections;
|
|
49
|
+
#recorder;
|
|
50
|
+
constructor(registry, upstreams, cursors, masterKey, projections, recorder) {
|
|
51
|
+
this.#registry = registry;
|
|
52
|
+
this.#upstreams = upstreams;
|
|
53
|
+
this.#cursors = cursors;
|
|
54
|
+
this.#projections = projections;
|
|
55
|
+
this.#recorder = recorder;
|
|
56
|
+
this.#stateCodec = createRequestStateCodec({
|
|
57
|
+
key: createHash('sha256').update(masterKey).digest(),
|
|
58
|
+
ttlSeconds: 86_400,
|
|
59
|
+
bind: (context) => `${context.mcpReq.method}\0${context.http?.authInfo?.clientId ?? 'anonymous'}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
aggregate() {
|
|
63
|
+
const aggregate = this.#registry.aggregate();
|
|
64
|
+
const server = new Server({ name: 'toolhome', version: '0.1.0', title: 'ToolHome' }, {
|
|
65
|
+
capabilities: aggregate.capabilities,
|
|
66
|
+
instructions: 'ToolHome aggregates enabled servers. Tools and prompts use server_slug.name. Resources use toolhome:// virtual URIs. Use an individual /mcp/{server_slug} endpoint for exact upstream names and extension semantics.',
|
|
67
|
+
requestState: { verify: this.#stateCodec.verify },
|
|
68
|
+
inputRequired: { legacyShim: true },
|
|
69
|
+
});
|
|
70
|
+
this.#aggregateServers.add(server);
|
|
71
|
+
if (aggregate.capabilities.tools) {
|
|
72
|
+
server.setRequestHandler('tools/list', async (request, context) => {
|
|
73
|
+
const entries = this.#registry.entries();
|
|
74
|
+
const tools = await this.#aggregateTools(server, entries, context, request.params);
|
|
75
|
+
const page = this.#page(tools, request.params?.cursor, fingerprint({ tools }));
|
|
76
|
+
return {
|
|
77
|
+
tools: page.items,
|
|
78
|
+
...this.#nextCursor(page),
|
|
79
|
+
ttlMs: 0,
|
|
80
|
+
cacheScope: 'private',
|
|
81
|
+
_meta: { 'toolhome/server-count': entries.length },
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
server.setRequestHandler('tools/call', { params: CallToolRequestParamsSchema, result: gatewayCallResultSchema }, async (request, context) => {
|
|
85
|
+
const startedAt = new Date();
|
|
86
|
+
let route = null;
|
|
87
|
+
try {
|
|
88
|
+
route = await this.#liveToolRoute(server, request.name, context);
|
|
89
|
+
const params = this.#prepareParams(this.#restoreParams({ ...request, name: route.originalName }, route.entry.server.slug), context, route.entry.server.id, route.entry.server.slug);
|
|
90
|
+
const raw = await this.#execute(server, route.entry, { method: 'tools/call', params }, context);
|
|
91
|
+
this.#recordCall(context, {
|
|
92
|
+
endpointType: 'aggregate',
|
|
93
|
+
serverId: route.entry.server.id,
|
|
94
|
+
exposedToolName: request.name,
|
|
95
|
+
upstreamToolName: route.originalName,
|
|
96
|
+
status: 'success',
|
|
97
|
+
startedAt,
|
|
98
|
+
raw,
|
|
99
|
+
});
|
|
100
|
+
return this.#parseToolResult(raw, context, route.entry.server.id, route.entry.server.slug);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.#recordCallError(context, {
|
|
104
|
+
endpointType: 'aggregate',
|
|
105
|
+
serverId: route?.entry.server.id ?? null,
|
|
106
|
+
exposedToolName: request.name,
|
|
107
|
+
upstreamToolName: route?.originalName ?? request.name,
|
|
108
|
+
startedAt,
|
|
109
|
+
error,
|
|
110
|
+
});
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (aggregate.capabilities.prompts) {
|
|
116
|
+
server.setRequestHandler('prompts/list', async (request, context) => {
|
|
117
|
+
const prompts = await this.#aggregatePrompts(server, this.#registry.entries(), context, request.params);
|
|
118
|
+
const page = this.#page(prompts, request.params?.cursor, fingerprint({ prompts }));
|
|
119
|
+
return {
|
|
120
|
+
prompts: page.items,
|
|
121
|
+
...this.#nextCursor(page),
|
|
122
|
+
ttlMs: 0,
|
|
123
|
+
cacheScope: 'private',
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
server.setRequestHandler('prompts/get', async (request, context) => {
|
|
127
|
+
const route = await this.#livePromptRoute(server, request.params.name, context);
|
|
128
|
+
const params = this.#prepareParams(this.#restoreParams({ ...request.params, name: route.originalName }, route.entry.server.slug), context, route.entry.server.id, route.entry.server.slug);
|
|
129
|
+
const raw = await this.#execute(server, route.entry, { method: 'prompts/get', params }, context);
|
|
130
|
+
const result = await this.#parsePromptResult(raw, context, route.entry.server.id, route.entry.server.slug);
|
|
131
|
+
if (isInputRequiredResult(result))
|
|
132
|
+
return result;
|
|
133
|
+
return GetPromptResultSchema.parse(rewriteAggregateContent(result, route.entry.server.slug));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (aggregate.capabilities.resources) {
|
|
137
|
+
server.setRequestHandler('resources/list', async (request, context) => {
|
|
138
|
+
const resources = await this.#aggregateResources(server, this.#registry.entries(), context, request.params);
|
|
139
|
+
const page = this.#page(resources, request.params?.cursor, fingerprint({ resources }));
|
|
140
|
+
return {
|
|
141
|
+
resources: page.items,
|
|
142
|
+
...this.#nextCursor(page),
|
|
143
|
+
ttlMs: 0,
|
|
144
|
+
cacheScope: 'private',
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
server.setRequestHandler('resources/templates/list', async (request, context) => {
|
|
148
|
+
const resourceTemplates = await this.#aggregateResourceTemplates(server, this.#registry.entries(), context, request.params);
|
|
149
|
+
const page = this.#page(resourceTemplates, request.params?.cursor, fingerprint({ resourceTemplates }));
|
|
150
|
+
return {
|
|
151
|
+
resourceTemplates: page.items,
|
|
152
|
+
...this.#nextCursor(page),
|
|
153
|
+
ttlMs: 0,
|
|
154
|
+
cacheScope: 'private',
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
server.setRequestHandler('resources/read', async (request, context) => {
|
|
158
|
+
const route = this.#resourceRoute(request.params.uri);
|
|
159
|
+
const params = this.#prepareParams({ ...request.params, uri: route.upstreamUri }, context, route.entry.server.id, route.entry.server.slug);
|
|
160
|
+
const raw = await this.#execute(server, route.entry, { method: 'resources/read', params }, context);
|
|
161
|
+
const result = await this.#parseResourceResult(raw, context, route.entry.server.id, route.entry.server.slug);
|
|
162
|
+
if (isInputRequiredResult(result))
|
|
163
|
+
return result;
|
|
164
|
+
return ReadResourceResultSchema.parse(rewriteAggregateContent(result, route.entry.server.slug));
|
|
165
|
+
});
|
|
166
|
+
if (aggregate.capabilities.resources.subscribe) {
|
|
167
|
+
server.setRequestHandler('resources/subscribe', async (request, context) => {
|
|
168
|
+
const route = this.#resourceRoute(request.params.uri);
|
|
169
|
+
await this.#execute(server, route.entry, {
|
|
170
|
+
method: 'resources/subscribe',
|
|
171
|
+
params: { ...request.params, uri: route.upstreamUri },
|
|
172
|
+
}, context);
|
|
173
|
+
return {};
|
|
174
|
+
});
|
|
175
|
+
server.setRequestHandler('resources/unsubscribe', async (request, context) => {
|
|
176
|
+
const route = this.#resourceRoute(request.params.uri);
|
|
177
|
+
await this.#execute(server, route.entry, {
|
|
178
|
+
method: 'resources/unsubscribe',
|
|
179
|
+
params: { ...request.params, uri: route.upstreamUri },
|
|
180
|
+
}, context);
|
|
181
|
+
return {};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (aggregate.capabilities.completions) {
|
|
186
|
+
server.setRequestHandler('completion/complete', async (request, context) => {
|
|
187
|
+
const route = await (request.params.ref.type === 'ref/prompt'
|
|
188
|
+
? this.#promptCompletionRoute(server, request.params.ref.name, context)
|
|
189
|
+
: Promise.resolve(this.#resourceCompletionRoute(request.params.ref.uri)));
|
|
190
|
+
const params = this.#restoreParams({
|
|
191
|
+
...request.params,
|
|
192
|
+
ref: request.params.ref.type === 'ref/prompt'
|
|
193
|
+
? { ...request.params.ref, name: route.original }
|
|
194
|
+
: { ...request.params.ref, uri: route.original },
|
|
195
|
+
}, route.entry.server.slug);
|
|
196
|
+
const raw = await this.#execute(server, route.entry, { method: 'completion/complete', params }, context);
|
|
197
|
+
return CompleteResultSchema.parse(raw);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (aggregate.capabilities.logging) {
|
|
201
|
+
server.setRequestHandler('logging/setLevel', async (request, context) => {
|
|
202
|
+
const targets = this.#registry
|
|
203
|
+
.entries()
|
|
204
|
+
.filter(({ snapshot }) => snapshot.capabilities.logging);
|
|
205
|
+
const results = await Promise.allSettled(targets.map((entry) => this.#execute(server, entry, { method: 'logging/setLevel', params: request.params }, context)));
|
|
206
|
+
if (results.length > 0 && results.every((result) => result.status === 'rejected')) {
|
|
207
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, 'Every upstream rejected log level');
|
|
208
|
+
}
|
|
209
|
+
return {};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
server.fallbackRequestHandler = async (request, context) => {
|
|
213
|
+
const taskMethod = canonicalTaskMethod(request.method);
|
|
214
|
+
if (taskMethod) {
|
|
215
|
+
const params = taskParamsSchema.parse(request.params);
|
|
216
|
+
const route = parseVirtualTaskId(params.taskId);
|
|
217
|
+
if (!route) {
|
|
218
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Invalid ToolHome task ID');
|
|
219
|
+
}
|
|
220
|
+
const entry = this.#registry.entryBySlug(route.slug);
|
|
221
|
+
const raw = await this.#execute(server, entry, {
|
|
222
|
+
method: taskMethod,
|
|
223
|
+
params: this.#restoreParams({ ...params, taskId: route.upstreamTaskId }, route.slug),
|
|
224
|
+
}, context);
|
|
225
|
+
return ResultSchema.parse(rewriteAggregateTask(raw, route.slug));
|
|
226
|
+
}
|
|
227
|
+
const route = splitAggregateExtensionMethod(request.method);
|
|
228
|
+
if (!route) {
|
|
229
|
+
throw new ProtocolError(ProtocolErrorCode.MethodNotFound, 'Aggregate extension methods must use toolhome/{server_slug}/{upstream_method}');
|
|
230
|
+
}
|
|
231
|
+
const entry = this.#registry.entryBySlug(route.slug);
|
|
232
|
+
const raw = await this.#execute(server, entry, {
|
|
233
|
+
method: route.upstreamMethod,
|
|
234
|
+
...(request.params === undefined
|
|
235
|
+
? {}
|
|
236
|
+
: { params: this.#restoreParams(request.params, route.slug) }),
|
|
237
|
+
}, context);
|
|
238
|
+
return ResultSchema.parse(rewriteAggregateContent(raw, route.slug));
|
|
239
|
+
};
|
|
240
|
+
server.fallbackNotificationHandler = async (notification) => {
|
|
241
|
+
const route = splitAggregateExtensionMethod(notification.method);
|
|
242
|
+
if (!route)
|
|
243
|
+
return;
|
|
244
|
+
const entry = this.#registry.entryBySlug(route.slug);
|
|
245
|
+
const forwarded = {
|
|
246
|
+
method: route.upstreamMethod,
|
|
247
|
+
...(notification.params === undefined
|
|
248
|
+
? {}
|
|
249
|
+
: { params: this.#restoreParams(notification.params, route.slug) }),
|
|
250
|
+
};
|
|
251
|
+
await this.#upstreams.notifyDetached(entry.server.id, forwarded, this.#notificationClientCapabilities(notification, server));
|
|
252
|
+
};
|
|
253
|
+
this.#installClientNotificationBridges(server, () => this.#registry.entries());
|
|
254
|
+
return server;
|
|
255
|
+
}
|
|
256
|
+
individual(slug) {
|
|
257
|
+
const entry = this.#registry.entryBySlug(slug);
|
|
258
|
+
const snapshot = entry.snapshot;
|
|
259
|
+
const server = new Server({
|
|
260
|
+
name: `toolhome/${slug}`,
|
|
261
|
+
title: entry.server.name,
|
|
262
|
+
version: '0.1.0',
|
|
263
|
+
}, {
|
|
264
|
+
capabilities: snapshot.capabilities,
|
|
265
|
+
...(snapshot.instructions === null ? {} : { instructions: snapshot.instructions }),
|
|
266
|
+
requestState: { verify: this.#stateCodec.verify },
|
|
267
|
+
inputRequired: { legacyShim: true },
|
|
268
|
+
});
|
|
269
|
+
if (snapshot.capabilities.tools) {
|
|
270
|
+
server.setRequestHandler('tools/list', async (request, context) => {
|
|
271
|
+
const raw = await this.#execute(server, entry, { method: 'tools/list', params: request.params }, context);
|
|
272
|
+
return ListToolsResultSchema.parse(raw);
|
|
273
|
+
});
|
|
274
|
+
server.setRequestHandler('tools/call', { params: CallToolRequestParamsSchema, result: gatewayCallResultSchema }, async (request, context) => {
|
|
275
|
+
const startedAt = new Date();
|
|
276
|
+
try {
|
|
277
|
+
const raw = await this.#execute(server, entry, {
|
|
278
|
+
method: 'tools/call',
|
|
279
|
+
params: this.#prepareParams(request, context, entry.server.id),
|
|
280
|
+
}, context);
|
|
281
|
+
this.#recordCall(context, {
|
|
282
|
+
endpointType: 'individual',
|
|
283
|
+
serverId: entry.server.id,
|
|
284
|
+
exposedToolName: request.name,
|
|
285
|
+
upstreamToolName: request.name,
|
|
286
|
+
status: 'success',
|
|
287
|
+
startedAt,
|
|
288
|
+
raw,
|
|
289
|
+
});
|
|
290
|
+
return this.#parseToolResult(raw, context, entry.server.id, null);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
this.#recordCallError(context, {
|
|
294
|
+
endpointType: 'individual',
|
|
295
|
+
serverId: entry.server.id,
|
|
296
|
+
exposedToolName: request.name,
|
|
297
|
+
upstreamToolName: request.name,
|
|
298
|
+
startedAt,
|
|
299
|
+
error,
|
|
300
|
+
});
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (snapshot.capabilities.prompts) {
|
|
306
|
+
server.setRequestHandler('prompts/list', async (request, context) => {
|
|
307
|
+
const raw = await this.#execute(server, entry, { method: 'prompts/list', params: request.params }, context);
|
|
308
|
+
return ListPromptsResultSchema.parse(raw);
|
|
309
|
+
});
|
|
310
|
+
server.setRequestHandler('prompts/get', async (request, context) => {
|
|
311
|
+
const raw = await this.#execute(server, entry, {
|
|
312
|
+
method: 'prompts/get',
|
|
313
|
+
params: this.#prepareParams(request.params, context, entry.server.id),
|
|
314
|
+
}, context);
|
|
315
|
+
return this.#parsePromptResult(raw, context, entry.server.id);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (snapshot.capabilities.resources) {
|
|
319
|
+
server.setRequestHandler('resources/list', async (request, context) => {
|
|
320
|
+
const raw = await this.#execute(server, entry, { method: 'resources/list', params: request.params }, context);
|
|
321
|
+
return ListResourcesResultSchema.parse(raw);
|
|
322
|
+
});
|
|
323
|
+
server.setRequestHandler('resources/templates/list', async (request, context) => {
|
|
324
|
+
const raw = await this.#execute(server, entry, { method: 'resources/templates/list', params: request.params }, context);
|
|
325
|
+
return ListResourceTemplatesResultSchema.parse(raw);
|
|
326
|
+
});
|
|
327
|
+
server.setRequestHandler('resources/read', async (request, context) => {
|
|
328
|
+
const raw = await this.#execute(server, entry, {
|
|
329
|
+
method: 'resources/read',
|
|
330
|
+
params: this.#prepareParams(request.params, context, entry.server.id),
|
|
331
|
+
}, context);
|
|
332
|
+
return this.#parseResourceResult(raw, context, entry.server.id);
|
|
333
|
+
});
|
|
334
|
+
if (snapshot.capabilities.resources.subscribe) {
|
|
335
|
+
server.setRequestHandler('resources/subscribe', async (request, context) => {
|
|
336
|
+
await this.#execute(server, entry, { method: 'resources/subscribe', params: request.params }, context);
|
|
337
|
+
return {};
|
|
338
|
+
});
|
|
339
|
+
server.setRequestHandler('resources/unsubscribe', async (request, context) => {
|
|
340
|
+
await this.#execute(server, entry, { method: 'resources/unsubscribe', params: request.params }, context);
|
|
341
|
+
return {};
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (snapshot.capabilities.completions) {
|
|
346
|
+
server.setRequestHandler('completion/complete', async (request, context) => {
|
|
347
|
+
const raw = await this.#execute(server, entry, { method: 'completion/complete', params: request.params }, context);
|
|
348
|
+
return CompleteResultSchema.parse(raw);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (snapshot.capabilities.logging) {
|
|
352
|
+
server.setRequestHandler('logging/setLevel', async (request, context) => {
|
|
353
|
+
const raw = await this.#execute(server, entry, { method: 'logging/setLevel', params: request.params }, context);
|
|
354
|
+
return EmptyResultSchema.parse(raw);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
server.fallbackRequestHandler = async (request, context) => {
|
|
358
|
+
const taskMethod = canonicalTaskMethod(request.method);
|
|
359
|
+
const raw = await this.#execute(server, entry, { method: taskMethod ?? request.method, params: request.params }, context);
|
|
360
|
+
return ResultSchema.parse(raw);
|
|
361
|
+
};
|
|
362
|
+
server.fallbackNotificationHandler = async (notification) => {
|
|
363
|
+
await this.#upstreams.notifyDetached(entry.server.id, notification, this.#notificationClientCapabilities(notification, server));
|
|
364
|
+
};
|
|
365
|
+
this.#installClientNotificationBridges(server, () => [entry]);
|
|
366
|
+
return server;
|
|
367
|
+
}
|
|
368
|
+
async #execute(server, entry, request, context) {
|
|
369
|
+
try {
|
|
370
|
+
return await this.#upstreams.execute(entry.server.id, request, context, this.#requestClientCapabilities(server, context), this.#aggregateServers.has(server)
|
|
371
|
+
? {
|
|
372
|
+
transformClientResult: (value) => restoreAggregateContent(value, entry.server.slug),
|
|
373
|
+
transformNotification: (notification) => this.#aggregateNotification(notification, entry.server.slug),
|
|
374
|
+
transformRequest: (upstreamRequest) => this.#aggregateRequest(upstreamRequest, entry.server.slug),
|
|
375
|
+
}
|
|
376
|
+
: {});
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (ProtocolError.isInstance(error))
|
|
380
|
+
throw error;
|
|
381
|
+
if (error instanceof AppError) {
|
|
382
|
+
const code = error.status === 404
|
|
383
|
+
? ProtocolErrorCode.InvalidParams
|
|
384
|
+
: error.status >= 500
|
|
385
|
+
? ProtocolErrorCode.InternalError
|
|
386
|
+
: ProtocolErrorCode.InvalidParams;
|
|
387
|
+
throw new ProtocolError(code, error.message, {
|
|
388
|
+
source: entry.server.slug,
|
|
389
|
+
code: error.code,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
#aggregateNotification(notification, slug) {
|
|
396
|
+
const method = coreServerNotificationMethods.has(notification.method)
|
|
397
|
+
? notification.method
|
|
398
|
+
: aggregateExtensionMethod(slug, notification.method);
|
|
399
|
+
if (notification.params === undefined)
|
|
400
|
+
return { method };
|
|
401
|
+
let value = rewriteAggregateContent(notification.params, slug);
|
|
402
|
+
if (notification.method === 'notifications/resources/updated') {
|
|
403
|
+
const uri = notification.params.uri;
|
|
404
|
+
if (typeof uri === 'string' && isRecord(value)) {
|
|
405
|
+
value = { ...value, uri: virtualResourceUri(slug, uri) };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (notification.method === 'notifications/tasks/status' ||
|
|
409
|
+
notification.method === 'notifications/tasks') {
|
|
410
|
+
value = rewriteAggregateTask(notification.params, slug);
|
|
411
|
+
}
|
|
412
|
+
return isRecord(value) ? { method, params: value } : { method, params: notification.params };
|
|
413
|
+
}
|
|
414
|
+
#aggregateRequest(request, slug) {
|
|
415
|
+
const method = coreClientRequestMethods.has(request.method)
|
|
416
|
+
? request.method
|
|
417
|
+
: aggregateExtensionMethod(slug, request.method);
|
|
418
|
+
if (request.params === undefined)
|
|
419
|
+
return { method };
|
|
420
|
+
const rewritten = rewriteAggregateContent(request.params, slug);
|
|
421
|
+
return {
|
|
422
|
+
method,
|
|
423
|
+
params: isRecord(rewritten) ? rewritten : request.params,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
#requestClientCapabilities(server, context) {
|
|
427
|
+
const envelope = context.mcpReq.envelope;
|
|
428
|
+
if (envelope) {
|
|
429
|
+
const parsed = ClientCapabilitiesSchema.safeParse(Reflect.get(envelope, CLIENT_CAPABILITIES_META_KEY));
|
|
430
|
+
if (parsed.success)
|
|
431
|
+
return parsed.data;
|
|
432
|
+
}
|
|
433
|
+
return server.getClientCapabilities() ?? {};
|
|
434
|
+
}
|
|
435
|
+
#notificationClientCapabilities(notification, server) {
|
|
436
|
+
const metadata = notification.params?._meta;
|
|
437
|
+
if (metadata) {
|
|
438
|
+
const parsed = ClientCapabilitiesSchema.safeParse(Reflect.get(metadata, CLIENT_CAPABILITIES_META_KEY));
|
|
439
|
+
if (parsed.success)
|
|
440
|
+
return parsed.data;
|
|
441
|
+
}
|
|
442
|
+
return server.getClientCapabilities() ?? {};
|
|
443
|
+
}
|
|
444
|
+
#installClientNotificationBridges(server, entries) {
|
|
445
|
+
const forward = async (notification) => {
|
|
446
|
+
const capabilities = this.#notificationClientCapabilities(notification, server);
|
|
447
|
+
await Promise.allSettled(entries().map((entry) => this.#upstreams.notifyDetached(entry.server.id, notification, capabilities)));
|
|
448
|
+
};
|
|
449
|
+
server.setNotificationHandler('notifications/roots/list_changed', forward);
|
|
450
|
+
server.setNotificationHandler('notifications/elicitation/complete', forward);
|
|
451
|
+
}
|
|
452
|
+
#prepareParams(value, context, serverId, aggregateSlug) {
|
|
453
|
+
const state = context.mcpReq.requestState();
|
|
454
|
+
const aggregate = aggregateSlug !== undefined;
|
|
455
|
+
if (state && (state.serverId !== serverId || state.aggregate !== aggregate)) {
|
|
456
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Request state target mismatch');
|
|
457
|
+
}
|
|
458
|
+
const prepared = {
|
|
459
|
+
...value,
|
|
460
|
+
...(context.mcpReq.inputResponses === undefined
|
|
461
|
+
? {}
|
|
462
|
+
: { inputResponses: context.mcpReq.inputResponses }),
|
|
463
|
+
...(state?.upstreamRequestState === undefined
|
|
464
|
+
? {}
|
|
465
|
+
: { requestState: state.upstreamRequestState }),
|
|
466
|
+
};
|
|
467
|
+
return aggregateSlug === undefined ? prepared : this.#restoreParams(prepared, aggregateSlug);
|
|
468
|
+
}
|
|
469
|
+
#restoreParams(value, slug) {
|
|
470
|
+
const restored = restoreAggregateContent(value, slug);
|
|
471
|
+
if (!isRecord(restored)) {
|
|
472
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Invalid aggregate request params');
|
|
473
|
+
}
|
|
474
|
+
return restored;
|
|
475
|
+
}
|
|
476
|
+
async #parseToolResult(value, context, serverId, aggregateSlug) {
|
|
477
|
+
if (isInputRequiredResult(value)) {
|
|
478
|
+
return gatewayCallResultSchema.parse(await this.#wrapInputRequired(value, context, serverId, aggregateSlug));
|
|
479
|
+
}
|
|
480
|
+
if (extensionTaskResultSchema.safeParse(value).success ||
|
|
481
|
+
legacyTaskResultSchema.safeParse(value).success) {
|
|
482
|
+
return gatewayCallResultSchema.parse(aggregateSlug === null ? value : rewriteAggregateTask(value, aggregateSlug));
|
|
483
|
+
}
|
|
484
|
+
return gatewayCallResultSchema.parse(aggregateSlug === null ? value : rewriteAggregateContent(value, aggregateSlug));
|
|
485
|
+
}
|
|
486
|
+
async #parsePromptResult(value, context, serverId, aggregateSlug = null) {
|
|
487
|
+
if (isInputRequiredResult(value)) {
|
|
488
|
+
return this.#wrapInputRequired(value, context, serverId, aggregateSlug);
|
|
489
|
+
}
|
|
490
|
+
return GetPromptResultSchema.parse(value);
|
|
491
|
+
}
|
|
492
|
+
async #parseResourceResult(value, context, serverId, aggregateSlug = null) {
|
|
493
|
+
if (isInputRequiredResult(value)) {
|
|
494
|
+
return this.#wrapInputRequired(value, context, serverId, aggregateSlug);
|
|
495
|
+
}
|
|
496
|
+
return ReadResourceResultSchema.parse(value);
|
|
497
|
+
}
|
|
498
|
+
async #wrapInputRequired(value, context, serverId, aggregateSlug) {
|
|
499
|
+
const state = {
|
|
500
|
+
aggregate: aggregateSlug !== null,
|
|
501
|
+
serverId,
|
|
502
|
+
...(value.requestState === undefined ? {} : { upstreamRequestState: value.requestState }),
|
|
503
|
+
};
|
|
504
|
+
const rewritten = aggregateSlug === null ? value : rewriteAggregateContent(value, aggregateSlug);
|
|
505
|
+
if (!isInputRequiredResult(rewritten)) {
|
|
506
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, 'Invalid input-required result');
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
...rewritten,
|
|
510
|
+
requestState: await this.#stateCodec.mint(state, context),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
async #aggregateTools(server, entries, context, params) {
|
|
514
|
+
const groups = await Promise.all(entries
|
|
515
|
+
.filter(({ snapshot }) => snapshot.capabilities.tools)
|
|
516
|
+
.map(async (entry) => {
|
|
517
|
+
const tools = await this.#listTools(server, entry, context, params);
|
|
518
|
+
// Tool visibility is an aggregate-endpoint projection: hidden tools
|
|
519
|
+
// are excluded here (and enforced again in #liveToolRoute).
|
|
520
|
+
const visible = this.#projections.apply(entry.server.id, tools);
|
|
521
|
+
return visible.map((tool) => ({
|
|
522
|
+
...rewriteAggregateTool(tool, entry.server.slug),
|
|
523
|
+
name: aggregateName(entry.server.slug, tool.name),
|
|
524
|
+
}));
|
|
525
|
+
}));
|
|
526
|
+
return groups.flat().sort((left, right) => left.name.localeCompare(right.name));
|
|
527
|
+
}
|
|
528
|
+
async #aggregatePrompts(server, entries, context, params) {
|
|
529
|
+
const groups = await Promise.all(entries
|
|
530
|
+
.filter(({ snapshot }) => snapshot.capabilities.prompts)
|
|
531
|
+
.map(async (entry) => (await this.#listPrompts(server, entry, context, params)).map((prompt) => ({
|
|
532
|
+
...prompt,
|
|
533
|
+
name: aggregateName(entry.server.slug, prompt.name),
|
|
534
|
+
}))));
|
|
535
|
+
return groups.flat().sort((left, right) => left.name.localeCompare(right.name));
|
|
536
|
+
}
|
|
537
|
+
async #aggregateResources(server, entries, context, params) {
|
|
538
|
+
const groups = await Promise.all(entries
|
|
539
|
+
.filter(({ snapshot }) => snapshot.capabilities.resources)
|
|
540
|
+
.map(async (entry) => (await this.#listResources(server, entry, context, params)).map((resource) => ({
|
|
541
|
+
...resource,
|
|
542
|
+
uri: virtualResourceUri(entry.server.slug, resource.uri),
|
|
543
|
+
}))));
|
|
544
|
+
return groups.flat().sort((left, right) => left.uri.localeCompare(right.uri));
|
|
545
|
+
}
|
|
546
|
+
async #aggregateResourceTemplates(server, entries, context, params) {
|
|
547
|
+
const groups = await Promise.all(entries
|
|
548
|
+
.filter(({ snapshot }) => snapshot.capabilities.resources)
|
|
549
|
+
.map(async (entry) => (await this.#listResourceTemplates(server, entry, context, params)).map((template) => ({
|
|
550
|
+
...template,
|
|
551
|
+
uriTemplate: virtualResourceTemplate(entry.server.slug, template.uriTemplate),
|
|
552
|
+
}))));
|
|
553
|
+
return groups.flat().sort((left, right) => left.uriTemplate.localeCompare(right.uriTemplate));
|
|
554
|
+
}
|
|
555
|
+
async #listTools(server, entry, context, params) {
|
|
556
|
+
const tools = [];
|
|
557
|
+
let cursor;
|
|
558
|
+
const seen = new Set();
|
|
559
|
+
for (let page = 0; page < 256; page += 1) {
|
|
560
|
+
const raw = await this.#execute(server, entry, { method: 'tools/list', params: this.#listParams(params, cursor) }, context);
|
|
561
|
+
const result = ListToolsResultSchema.parse(raw);
|
|
562
|
+
tools.push(...result.tools);
|
|
563
|
+
if (result.nextCursor === undefined)
|
|
564
|
+
return tools;
|
|
565
|
+
this.#rememberCursor(seen, result.nextCursor, 'tools/list');
|
|
566
|
+
cursor = result.nextCursor;
|
|
567
|
+
}
|
|
568
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, 'tools/list exceeded 256 pages');
|
|
569
|
+
}
|
|
570
|
+
async #listPrompts(server, entry, context, params) {
|
|
571
|
+
const prompts = [];
|
|
572
|
+
let cursor;
|
|
573
|
+
const seen = new Set();
|
|
574
|
+
for (let page = 0; page < 256; page += 1) {
|
|
575
|
+
const raw = await this.#execute(server, entry, { method: 'prompts/list', params: this.#listParams(params, cursor) }, context);
|
|
576
|
+
const result = ListPromptsResultSchema.parse(raw);
|
|
577
|
+
prompts.push(...result.prompts);
|
|
578
|
+
if (result.nextCursor === undefined)
|
|
579
|
+
return prompts;
|
|
580
|
+
this.#rememberCursor(seen, result.nextCursor, 'prompts/list');
|
|
581
|
+
cursor = result.nextCursor;
|
|
582
|
+
}
|
|
583
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, 'prompts/list exceeded 256 pages');
|
|
584
|
+
}
|
|
585
|
+
async #listResources(server, entry, context, params) {
|
|
586
|
+
const resources = [];
|
|
587
|
+
let cursor;
|
|
588
|
+
const seen = new Set();
|
|
589
|
+
for (let page = 0; page < 256; page += 1) {
|
|
590
|
+
const raw = await this.#execute(server, entry, { method: 'resources/list', params: this.#listParams(params, cursor) }, context);
|
|
591
|
+
const result = ListResourcesResultSchema.parse(raw);
|
|
592
|
+
resources.push(...result.resources);
|
|
593
|
+
if (result.nextCursor === undefined)
|
|
594
|
+
return resources;
|
|
595
|
+
this.#rememberCursor(seen, result.nextCursor, 'resources/list');
|
|
596
|
+
cursor = result.nextCursor;
|
|
597
|
+
}
|
|
598
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, 'resources/list exceeded 256 pages');
|
|
599
|
+
}
|
|
600
|
+
async #listResourceTemplates(server, entry, context, params) {
|
|
601
|
+
const templates = [];
|
|
602
|
+
let cursor;
|
|
603
|
+
const seen = new Set();
|
|
604
|
+
for (let page = 0; page < 256; page += 1) {
|
|
605
|
+
const raw = await this.#execute(server, entry, { method: 'resources/templates/list', params: this.#listParams(params, cursor) }, context);
|
|
606
|
+
const result = ListResourceTemplatesResultSchema.parse(raw);
|
|
607
|
+
templates.push(...result.resourceTemplates);
|
|
608
|
+
if (result.nextCursor === undefined)
|
|
609
|
+
return templates;
|
|
610
|
+
this.#rememberCursor(seen, result.nextCursor, 'resources/templates/list');
|
|
611
|
+
cursor = result.nextCursor;
|
|
612
|
+
}
|
|
613
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, 'resources/templates/list exceeded 256 pages');
|
|
614
|
+
}
|
|
615
|
+
async #liveToolRoute(server, name, context) {
|
|
616
|
+
const appRoute = this.#appResourceRoute(context.mcpReq._meta);
|
|
617
|
+
if (appRoute) {
|
|
618
|
+
const entry = this.#registry.entryBySlug(appRoute.slug);
|
|
619
|
+
const tool = (await this.#listTools(server, entry, context, {})).find((candidate) => candidate.name === name || aggregateName(entry.server.slug, candidate.name) === name);
|
|
620
|
+
if (tool && this.#projections.isVisible(entry.server.id, tool.name)) {
|
|
621
|
+
return { entry, originalName: tool.name };
|
|
622
|
+
}
|
|
623
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${name}`);
|
|
624
|
+
}
|
|
625
|
+
const parsed = splitAggregateName(name);
|
|
626
|
+
if (parsed) {
|
|
627
|
+
const entry = this.#registry.entryBySlug(parsed.slug);
|
|
628
|
+
const tool = (await this.#listTools(server, entry, context, {})).find((candidate) => aggregateName(entry.server.slug, candidate.name) === name);
|
|
629
|
+
if (tool && this.#projections.isVisible(entry.server.id, tool.name)) {
|
|
630
|
+
return { entry, originalName: tool.name };
|
|
631
|
+
}
|
|
632
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${name}`);
|
|
633
|
+
}
|
|
634
|
+
const candidates = (await Promise.all(this.#registry.entries().map(async (entry) => ({
|
|
635
|
+
entry,
|
|
636
|
+
tool: (await this.#listTools(server, entry, context, {})).find((candidate) => candidate.name === name && this.#projections.isVisible(entry.server.id, candidate.name)),
|
|
637
|
+
})))).filter((candidate) => candidate.tool !== undefined);
|
|
638
|
+
if (candidates.length === 1 && candidates[0]?.tool) {
|
|
639
|
+
return { entry: candidates[0].entry, originalName: candidates[0].tool.name };
|
|
640
|
+
}
|
|
641
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, candidates.length > 1
|
|
642
|
+
? `Tool name is ambiguous in the aggregate endpoint: ${name}`
|
|
643
|
+
: `Unknown aggregate tool: ${name}`);
|
|
644
|
+
}
|
|
645
|
+
#recordCall(context, input) {
|
|
646
|
+
const completedAt = new Date();
|
|
647
|
+
const toolError = isRecord(input.raw) && input.raw.isError === true;
|
|
648
|
+
this.#recorder.record({
|
|
649
|
+
endpointType: input.endpointType,
|
|
650
|
+
principalKind: this.#principalKind(context),
|
|
651
|
+
principalId: context.http?.authInfo?.clientId ?? 'anonymous',
|
|
652
|
+
serverId: input.serverId,
|
|
653
|
+
exposedToolName: input.exposedToolName,
|
|
654
|
+
upstreamToolName: input.upstreamToolName,
|
|
655
|
+
status: toolError ? 'tool_error' : input.status,
|
|
656
|
+
errorType: toolError ? 'tool_error' : null,
|
|
657
|
+
startedAt: input.startedAt.toISOString(),
|
|
658
|
+
completedAt: completedAt.toISOString(),
|
|
659
|
+
durationMs: Math.max(0, completedAt.getTime() - input.startedAt.getTime()),
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
#recordCallError(context, input) {
|
|
663
|
+
const completedAt = new Date();
|
|
664
|
+
this.#recorder.record({
|
|
665
|
+
endpointType: input.endpointType,
|
|
666
|
+
principalKind: this.#principalKind(context),
|
|
667
|
+
principalId: context.http?.authInfo?.clientId ?? 'anonymous',
|
|
668
|
+
serverId: input.serverId,
|
|
669
|
+
exposedToolName: input.exposedToolName,
|
|
670
|
+
upstreamToolName: input.upstreamToolName,
|
|
671
|
+
status: this.#errorStatus(input.error),
|
|
672
|
+
errorType: this.#errorCode(input.error),
|
|
673
|
+
startedAt: input.startedAt.toISOString(),
|
|
674
|
+
completedAt: completedAt.toISOString(),
|
|
675
|
+
durationMs: Math.max(0, completedAt.getTime() - input.startedAt.getTime()),
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
#principalKind(context) {
|
|
679
|
+
const kind = context.http?.authInfo?.extra
|
|
680
|
+
?.credentialKind;
|
|
681
|
+
if (kind === 'control')
|
|
682
|
+
return 'control_key';
|
|
683
|
+
if (kind === 'access')
|
|
684
|
+
return 'access_key';
|
|
685
|
+
return 'oauth_client';
|
|
686
|
+
}
|
|
687
|
+
#errorStatus(error) {
|
|
688
|
+
if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout)
|
|
689
|
+
return 'timeout';
|
|
690
|
+
return 'protocol_error';
|
|
691
|
+
}
|
|
692
|
+
#errorCode(error) {
|
|
693
|
+
if (error instanceof ProtocolError)
|
|
694
|
+
return String(error.code);
|
|
695
|
+
if (error instanceof SdkError)
|
|
696
|
+
return error.code;
|
|
697
|
+
if (error instanceof AppError)
|
|
698
|
+
return error.code;
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
#appResourceRoute(metadata) {
|
|
702
|
+
if (!metadata)
|
|
703
|
+
return null;
|
|
704
|
+
const legacy = metadata['ui/resourceUri'];
|
|
705
|
+
if (typeof legacy === 'string')
|
|
706
|
+
return parseVirtualResourceUri(legacy);
|
|
707
|
+
const ui = metadata.ui;
|
|
708
|
+
if (ui === null || typeof ui !== 'object' || Array.isArray(ui))
|
|
709
|
+
return null;
|
|
710
|
+
const resourceUri = Reflect.get(ui, 'resourceUri');
|
|
711
|
+
return typeof resourceUri === 'string' ? parseVirtualResourceUri(resourceUri) : null;
|
|
712
|
+
}
|
|
713
|
+
async #livePromptRoute(server, name, context) {
|
|
714
|
+
const parsed = splitAggregateName(name);
|
|
715
|
+
if (!parsed)
|
|
716
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Invalid aggregate prompt name');
|
|
717
|
+
const entry = this.#registry.entryBySlug(parsed.slug);
|
|
718
|
+
const prompt = (await this.#listPrompts(server, entry, context, {})).find((candidate) => aggregateName(entry.server.slug, candidate.name) === name);
|
|
719
|
+
if (!prompt)
|
|
720
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown prompt: ${name}`);
|
|
721
|
+
return { entry, originalName: prompt.name };
|
|
722
|
+
}
|
|
723
|
+
#listParams(value, cursor) {
|
|
724
|
+
const parsed = paramsSchema.safeParse(value);
|
|
725
|
+
const base = parsed.success
|
|
726
|
+
? Object.fromEntries(Object.entries(parsed.data).filter(([key]) => key !== 'cursor'))
|
|
727
|
+
: {};
|
|
728
|
+
return cursor === undefined ? base : { ...base, cursor };
|
|
729
|
+
}
|
|
730
|
+
#rememberCursor(seen, cursor, method) {
|
|
731
|
+
if (seen.has(cursor)) {
|
|
732
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, `${method} repeated a cursor`);
|
|
733
|
+
}
|
|
734
|
+
seen.add(cursor);
|
|
735
|
+
}
|
|
736
|
+
#resourceRoute(uri) {
|
|
737
|
+
const parsed = parseVirtualResourceUri(uri) ?? expandVirtualResourceTemplate(uri);
|
|
738
|
+
if (!parsed)
|
|
739
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Invalid ToolHome URI');
|
|
740
|
+
return {
|
|
741
|
+
entry: this.#registry.entryBySlug(parsed.slug),
|
|
742
|
+
upstreamUri: parsed.upstreamUri,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
async #promptCompletionRoute(server, name, context) {
|
|
746
|
+
const route = await this.#livePromptRoute(server, name, context);
|
|
747
|
+
return { entry: route.entry, original: route.originalName };
|
|
748
|
+
}
|
|
749
|
+
#resourceCompletionRoute(uri) {
|
|
750
|
+
const template = parseVirtualResourceTemplate(uri);
|
|
751
|
+
if (template) {
|
|
752
|
+
return {
|
|
753
|
+
entry: this.#registry.entryBySlug(template.slug),
|
|
754
|
+
original: template.upstreamTemplate,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const route = this.#resourceRoute(uri);
|
|
758
|
+
return { entry: route.entry, original: route.upstreamUri };
|
|
759
|
+
}
|
|
760
|
+
#page(items, cursor, key) {
|
|
761
|
+
const offset = cursor === undefined ? 0 : this.#cursors.decode(cursor, key).offset;
|
|
762
|
+
const pageItems = items.slice(offset, offset + pageSize);
|
|
763
|
+
const nextOffset = offset + pageItems.length;
|
|
764
|
+
return {
|
|
765
|
+
items: pageItems,
|
|
766
|
+
...(nextOffset >= items.length
|
|
767
|
+
? {}
|
|
768
|
+
: { nextCursor: this.#cursors.encode({ key, offset: nextOffset }) }),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
#nextCursor(page) {
|
|
772
|
+
return page.nextCursor === undefined ? {} : { nextCursor: page.nextCursor };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const coreServerNotificationMethods = new Set([
|
|
776
|
+
'notifications/cancelled',
|
|
777
|
+
'notifications/progress',
|
|
778
|
+
'notifications/message',
|
|
779
|
+
'notifications/resources/updated',
|
|
780
|
+
'notifications/resources/list_changed',
|
|
781
|
+
'notifications/tools/list_changed',
|
|
782
|
+
'notifications/prompts/list_changed',
|
|
783
|
+
'notifications/subscriptions/acknowledged',
|
|
784
|
+
'notifications/tasks/status',
|
|
785
|
+
'notifications/tasks',
|
|
786
|
+
]);
|
|
787
|
+
const coreClientRequestMethods = new Set([
|
|
788
|
+
'ping',
|
|
789
|
+
'roots/list',
|
|
790
|
+
'sampling/createMessage',
|
|
791
|
+
'elicitation/create',
|
|
792
|
+
]);
|
|
793
|
+
function isRecord(value) {
|
|
794
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
795
|
+
}
|
|
796
|
+
//# sourceMappingURL=gateway-server.js.map
|