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.
Files changed (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/README.zh.md +281 -0
  4. package/dist/server/app.d.ts +16 -0
  5. package/dist/server/app.d.ts.map +1 -0
  6. package/dist/server/app.js +166 -0
  7. package/dist/server/app.js.map +1 -0
  8. package/dist/server/cli/main.d.ts +3 -0
  9. package/dist/server/cli/main.d.ts.map +1 -0
  10. package/dist/server/cli/main.js +587 -0
  11. package/dist/server/cli/main.js.map +1 -0
  12. package/dist/server/cli-plane/allow-list.d.ts +18 -0
  13. package/dist/server/cli-plane/allow-list.d.ts.map +1 -0
  14. package/dist/server/cli-plane/allow-list.js +28 -0
  15. package/dist/server/cli-plane/allow-list.js.map +1 -0
  16. package/dist/server/cli-plane/cli-service.d.ts +41 -0
  17. package/dist/server/cli-plane/cli-service.d.ts.map +1 -0
  18. package/dist/server/cli-plane/cli-service.js +282 -0
  19. package/dist/server/cli-plane/cli-service.js.map +1 -0
  20. package/dist/server/cli-plane/frames.d.ts +26 -0
  21. package/dist/server/cli-plane/frames.d.ts.map +1 -0
  22. package/dist/server/cli-plane/frames.js +12 -0
  23. package/dist/server/cli-plane/frames.js.map +1 -0
  24. package/dist/server/cli-plane/models.d.ts +174 -0
  25. package/dist/server/cli-plane/models.d.ts.map +1 -0
  26. package/dist/server/cli-plane/models.js +169 -0
  27. package/dist/server/cli-plane/models.js.map +1 -0
  28. package/dist/server/cli-plane/routes.d.ts +32 -0
  29. package/dist/server/cli-plane/routes.d.ts.map +1 -0
  30. package/dist/server/cli-plane/routes.js +94 -0
  31. package/dist/server/cli-plane/routes.js.map +1 -0
  32. package/dist/server/cli-plane/runner.d.ts +41 -0
  33. package/dist/server/cli-plane/runner.d.ts.map +1 -0
  34. package/dist/server/cli-plane/runner.js +163 -0
  35. package/dist/server/cli-plane/runner.js.map +1 -0
  36. package/dist/server/cli-plane/status.d.ts +12 -0
  37. package/dist/server/cli-plane/status.d.ts.map +1 -0
  38. package/dist/server/cli-plane/status.js +24 -0
  39. package/dist/server/cli-plane/status.js.map +1 -0
  40. package/dist/server/config/harness-import.d.ts +44 -0
  41. package/dist/server/config/harness-import.d.ts.map +1 -0
  42. package/dist/server/config/harness-import.js +166 -0
  43. package/dist/server/config/harness-import.js.map +1 -0
  44. package/dist/server/config.d.ts +20 -0
  45. package/dist/server/config.d.ts.map +1 -0
  46. package/dist/server/config.js +80 -0
  47. package/dist/server/config.js.map +1 -0
  48. package/dist/server/control/client.d.ts +24 -0
  49. package/dist/server/control/client.d.ts.map +1 -0
  50. package/dist/server/control/client.js +115 -0
  51. package/dist/server/control/client.js.map +1 -0
  52. package/dist/server/control/control-api.d.ts +39 -0
  53. package/dist/server/control/control-api.d.ts.map +1 -0
  54. package/dist/server/control/control-api.js +291 -0
  55. package/dist/server/control/control-api.js.map +1 -0
  56. package/dist/server/control/control-service.d.ts +713 -0
  57. package/dist/server/control/control-service.d.ts.map +1 -0
  58. package/dist/server/control/control-service.js +693 -0
  59. package/dist/server/control/control-service.js.map +1 -0
  60. package/dist/server/control/openapi.d.ts +2 -0
  61. package/dist/server/control/openapi.d.ts.map +1 -0
  62. package/dist/server/control/openapi.js +370 -0
  63. package/dist/server/control/openapi.js.map +1 -0
  64. package/dist/server/data-plane/data-plane.d.ts +20 -0
  65. package/dist/server/data-plane/data-plane.d.ts.map +1 -0
  66. package/dist/server/data-plane/data-plane.js +335 -0
  67. package/dist/server/data-plane/data-plane.js.map +1 -0
  68. package/dist/server/data-plane/gateway-server.d.ts +13 -0
  69. package/dist/server/data-plane/gateway-server.d.ts.map +1 -0
  70. package/dist/server/data-plane/gateway-server.js +796 -0
  71. package/dist/server/data-plane/gateway-server.js.map +1 -0
  72. package/dist/server/data-plane/legacy-session.d.ts +18 -0
  73. package/dist/server/data-plane/legacy-session.d.ts.map +1 -0
  74. package/dist/server/data-plane/legacy-session.js +137 -0
  75. package/dist/server/data-plane/legacy-session.js.map +1 -0
  76. package/dist/server/data-plane/projection.d.ts +18 -0
  77. package/dist/server/data-plane/projection.d.ts.map +1 -0
  78. package/dist/server/data-plane/projection.js +44 -0
  79. package/dist/server/data-plane/projection.js.map +1 -0
  80. package/dist/server/data-plane/registry.d.ts +31 -0
  81. package/dist/server/data-plane/registry.d.ts.map +1 -0
  82. package/dist/server/data-plane/registry.js +129 -0
  83. package/dist/server/data-plane/registry.js.map +1 -0
  84. package/dist/server/data-plane/task-extension.d.ts +7 -0
  85. package/dist/server/data-plane/task-extension.d.ts.map +1 -0
  86. package/dist/server/data-plane/task-extension.js +33 -0
  87. package/dist/server/data-plane/task-extension.js.map +1 -0
  88. package/dist/server/data-plane/virtualization.d.ts +35 -0
  89. package/dist/server/data-plane/virtualization.d.ts.map +1 -0
  90. package/dist/server/data-plane/virtualization.js +228 -0
  91. package/dist/server/data-plane/virtualization.js.map +1 -0
  92. package/dist/server/domain/errors.d.ts +9 -0
  93. package/dist/server/domain/errors.d.ts.map +1 -0
  94. package/dist/server/domain/errors.js +19 -0
  95. package/dist/server/domain/errors.js.map +1 -0
  96. package/dist/server/domain/models.d.ts +720 -0
  97. package/dist/server/domain/models.d.ts.map +1 -0
  98. package/dist/server/domain/models.js +327 -0
  99. package/dist/server/domain/models.js.map +1 -0
  100. package/dist/server/main.d.ts +2 -0
  101. package/dist/server/main.d.ts.map +1 -0
  102. package/dist/server/main.js +37 -0
  103. package/dist/server/main.js.map +1 -0
  104. package/dist/server/manage/management-mcp.d.ts +27 -0
  105. package/dist/server/manage/management-mcp.d.ts.map +1 -0
  106. package/dist/server/manage/management-mcp.js +431 -0
  107. package/dist/server/manage/management-mcp.js.map +1 -0
  108. package/dist/server/market/catalog.d.ts +111 -0
  109. package/dist/server/market/catalog.d.ts.map +1 -0
  110. package/dist/server/market/catalog.js +784 -0
  111. package/dist/server/market/catalog.js.map +1 -0
  112. package/dist/server/market/installers.d.ts +64 -0
  113. package/dist/server/market/installers.d.ts.map +1 -0
  114. package/dist/server/market/installers.js +181 -0
  115. package/dist/server/market/installers.js.map +1 -0
  116. package/dist/server/market/market-service.d.ts +146 -0
  117. package/dist/server/market/market-service.d.ts.map +1 -0
  118. package/dist/server/market/market-service.js +930 -0
  119. package/dist/server/market/market-service.js.map +1 -0
  120. package/dist/server/observability/call-recorder.d.ts +22 -0
  121. package/dist/server/observability/call-recorder.d.ts.map +1 -0
  122. package/dist/server/observability/call-recorder.js +99 -0
  123. package/dist/server/observability/call-recorder.js.map +1 -0
  124. package/dist/server/observability/logger.d.ts +16 -0
  125. package/dist/server/observability/logger.d.ts.map +1 -0
  126. package/dist/server/observability/logger.js +25 -0
  127. package/dist/server/observability/logger.js.map +1 -0
  128. package/dist/server/security/api-keys.d.ts +15 -0
  129. package/dist/server/security/api-keys.d.ts.map +1 -0
  130. package/dist/server/security/api-keys.js +35 -0
  131. package/dist/server/security/api-keys.js.map +1 -0
  132. package/dist/server/security/auth-service.d.ts +26 -0
  133. package/dist/server/security/auth-service.d.ts.map +1 -0
  134. package/dist/server/security/auth-service.js +76 -0
  135. package/dist/server/security/auth-service.js.map +1 -0
  136. package/dist/server/security/control-session.d.ts +9 -0
  137. package/dist/server/security/control-session.d.ts.map +1 -0
  138. package/dist/server/security/control-session.js +51 -0
  139. package/dist/server/security/control-session.js.map +1 -0
  140. package/dist/server/security/credential-materializer.d.ts +20 -0
  141. package/dist/server/security/credential-materializer.d.ts.map +1 -0
  142. package/dist/server/security/credential-materializer.js +82 -0
  143. package/dist/server/security/credential-materializer.js.map +1 -0
  144. package/dist/server/security/cursor-codec.d.ts +13 -0
  145. package/dist/server/security/cursor-codec.d.ts.map +1 -0
  146. package/dist/server/security/cursor-codec.js +43 -0
  147. package/dist/server/security/cursor-codec.js.map +1 -0
  148. package/dist/server/security/oauth-routes.d.ts +21 -0
  149. package/dist/server/security/oauth-routes.d.ts.map +1 -0
  150. package/dist/server/security/oauth-routes.js +100 -0
  151. package/dist/server/security/oauth-routes.js.map +1 -0
  152. package/dist/server/security/oauth-server.d.ts +16 -0
  153. package/dist/server/security/oauth-server.d.ts.map +1 -0
  154. package/dist/server/security/oauth-server.js +544 -0
  155. package/dist/server/security/oauth-server.js.map +1 -0
  156. package/dist/server/security/secret-box.d.ts +7 -0
  157. package/dist/server/security/secret-box.d.ts.map +1 -0
  158. package/dist/server/security/secret-box.js +27 -0
  159. package/dist/server/security/secret-box.js.map +1 -0
  160. package/dist/server/security/secure-action.d.ts +21 -0
  161. package/dist/server/security/secure-action.d.ts.map +1 -0
  162. package/dist/server/security/secure-action.js +69 -0
  163. package/dist/server/security/secure-action.js.map +1 -0
  164. package/dist/server/storage/sqlite-store.d.ts +70 -0
  165. package/dist/server/storage/sqlite-store.d.ts.map +1 -0
  166. package/dist/server/storage/sqlite-store.js +1522 -0
  167. package/dist/server/storage/sqlite-store.js.map +1 -0
  168. package/dist/server/storage/store.d.ts +82 -0
  169. package/dist/server/storage/store.d.ts.map +1 -0
  170. package/dist/server/storage/store.js +2 -0
  171. package/dist/server/storage/store.js.map +1 -0
  172. package/dist/server/upstream/adapter.d.ts +52 -0
  173. package/dist/server/upstream/adapter.d.ts.map +1 -0
  174. package/dist/server/upstream/adapter.js +807 -0
  175. package/dist/server/upstream/adapter.js.map +1 -0
  176. package/dist/server/upstream/credential-resolver.d.ts +14 -0
  177. package/dist/server/upstream/credential-resolver.d.ts.map +1 -0
  178. package/dist/server/upstream/credential-resolver.js +56 -0
  179. package/dist/server/upstream/credential-resolver.js.map +1 -0
  180. package/dist/server/upstream/extension-transport.d.ts +17 -0
  181. package/dist/server/upstream/extension-transport.d.ts.map +1 -0
  182. package/dist/server/upstream/extension-transport.js +207 -0
  183. package/dist/server/upstream/extension-transport.js.map +1 -0
  184. package/dist/server/upstream/manager.d.ts +23 -0
  185. package/dist/server/upstream/manager.d.ts.map +1 -0
  186. package/dist/server/upstream/manager.js +324 -0
  187. package/dist/server/upstream/manager.js.map +1 -0
  188. package/dist/server/upstream/oauth-provider.d.ts +38 -0
  189. package/dist/server/upstream/oauth-provider.d.ts.map +1 -0
  190. package/dist/server/upstream/oauth-provider.js +225 -0
  191. package/dist/server/upstream/oauth-provider.js.map +1 -0
  192. package/dist/server/upstream/oauth-refresh.d.ts +65 -0
  193. package/dist/server/upstream/oauth-refresh.d.ts.map +1 -0
  194. package/dist/server/upstream/oauth-refresh.js +240 -0
  195. package/dist/server/upstream/oauth-refresh.js.map +1 -0
  196. package/dist/server/upstream/oauth-routes.d.ts +17 -0
  197. package/dist/server/upstream/oauth-routes.d.ts.map +1 -0
  198. package/dist/server/upstream/oauth-routes.js +87 -0
  199. package/dist/server/upstream/oauth-routes.js.map +1 -0
  200. package/dist/server/upstream/oauth-service.d.ts +143 -0
  201. package/dist/server/upstream/oauth-service.d.ts.map +1 -0
  202. package/dist/server/upstream/oauth-service.js +217 -0
  203. package/dist/server/upstream/oauth-service.js.map +1 -0
  204. package/dist/server/upstream/stable-json.d.ts +3 -0
  205. package/dist/server/upstream/stable-json.d.ts.map +1 -0
  206. package/dist/server/upstream/stable-json.js +17 -0
  207. package/dist/server/upstream/stable-json.js.map +1 -0
  208. package/package.json +69 -0
  209. package/skills/toolhome/SKILL.md +238 -0
  210. package/skills/toolhome/references/cli-reference.md +242 -0
  211. package/skills/toolhome/references/deployment.md +113 -0
  212. package/skills/toolhome/references/market-guide.md +128 -0
  213. package/skills/toolhome/references/oauth-guide.md +96 -0
  214. package/skills/toolhome/references/troubleshooting.md +133 -0
@@ -0,0 +1,807 @@
1
+ import { Client, SSEClientTransport, StreamableHTTPClientTransport, withInputRequired, } from '@modelcontextprotocol/client';
2
+ import { CreateMessageResultSchema, CreateMessageResultWithToolsSchema, ElicitResultSchema, ListRootsResultSchema, } from '@modelcontextprotocol/core';
3
+ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { z } from 'zod';
6
+ import { AppError, errorMessage, toError } from '../domain/errors.js';
7
+ import { ExtensionTransportBridge, extensionFetch, isTaskExtensionMethod, restoreTaskResult, } from './extension-transport.js';
8
+ import { fingerprint, stableJson } from './stable-json.js';
9
+ const objectResultSchema = z.record(z.string(), z.unknown());
10
+ const inputRequiredResultSchema = withInputRequired(objectResultSchema);
11
+ const samplingResultSchema = z.union([
12
+ CreateMessageResultWithToolsSchema,
13
+ CreateMessageResultSchema,
14
+ ]);
15
+ const legacyRoundStatePrefix = 'toolhome-legacy-round:';
16
+ const mrtrMethods = new Set(['tools/call', 'prompts/get', 'resources/read']);
17
+ const discoveryCapabilities = {
18
+ extensions: {
19
+ 'io.modelcontextprotocol/tasks': {},
20
+ 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] },
21
+ },
22
+ };
23
+ export class UpstreamAdapter {
24
+ server;
25
+ #slots = [];
26
+ #creating = new Map();
27
+ #pendingConnections = new Set();
28
+ #waiters = new Set();
29
+ #resourceSubscriptions = new Map();
30
+ #subscriptionCreates = new Map();
31
+ #legacyRounds = new Map();
32
+ #credentials;
33
+ #logger;
34
+ #events;
35
+ #closed = false;
36
+ #generation = 0;
37
+ #restartPromise = null;
38
+ constructor(server, credentials, logger, events) {
39
+ this.server = server;
40
+ this.#credentials = credentials;
41
+ this.#logger = logger;
42
+ this.#events = events;
43
+ }
44
+ processId() {
45
+ for (const slot of this.#slots) {
46
+ if (slot.transport instanceof StdioClientTransport)
47
+ return slot.transport.pid;
48
+ }
49
+ return null;
50
+ }
51
+ async execute(request, bridge) {
52
+ if (request.method === 'resources/subscribe') {
53
+ await this.#retainResource(bridge.clientCapabilities, this.#requestUri(request));
54
+ return {};
55
+ }
56
+ if (request.method === 'resources/unsubscribe') {
57
+ await this.#releaseResource(bridge.clientCapabilities, this.#requestUri(request));
58
+ return {};
59
+ }
60
+ const legacyRoundId = this.#legacyRoundId(request.params?.requestState);
61
+ if (legacyRoundId !== null) {
62
+ return this.#resumeLegacyRound(legacyRoundId, request, bridge);
63
+ }
64
+ const slot = await this.#acquire(bridge.clientCapabilities);
65
+ slot.bridge.current = bridge;
66
+ if (slot.client.getProtocolEra() === 'legacy' &&
67
+ contextIsModern(bridge.context) &&
68
+ mrtrMethods.has(request.method)) {
69
+ return this.#startLegacyRound(slot, request, bridge);
70
+ }
71
+ try {
72
+ const options = this.#requestOptions(request, bridge.context);
73
+ return await this.#requestOnSlot(slot, request, options);
74
+ }
75
+ finally {
76
+ slot.bridge.current = null;
77
+ this.#release(slot);
78
+ }
79
+ }
80
+ async #requestOnSlot(slot, request, options) {
81
+ if (slot.client.getProtocolEra() === 'modern' && isTaskExtensionMethod(request.method)) {
82
+ return slot.extensions.request(request, options);
83
+ }
84
+ if (request.method === 'tools/call' && slot.client.getProtocolEra() === 'modern') {
85
+ // SEP-2243: callTool mirrors declared parameters into Mcp-Param-* headers
86
+ // and auto-retries HeaderMismatch (-32020) after refreshing tools/list.
87
+ // The low-level request() path does neither, which breaks upstreams that
88
+ // require header-mirrored params (e.g. GitHub's get_file_contents).
89
+ const result = await slot.client.callTool(request.params, {
90
+ ...options,
91
+ allowInputRequired: true,
92
+ });
93
+ return restoreTaskResult(result);
94
+ }
95
+ if (mrtrMethods.has(request.method)) {
96
+ const result = await slot.client.request(request, inputRequiredResultSchema, {
97
+ ...options,
98
+ allowInputRequired: true,
99
+ });
100
+ return restoreTaskResult(result);
101
+ }
102
+ return restoreTaskResult(await slot.client.request(request, objectResultSchema, options));
103
+ }
104
+ async #startLegacyRound(slot, request, bridge) {
105
+ const id = randomUUID();
106
+ const controller = new AbortController();
107
+ const completion = deferred();
108
+ const round = {
109
+ id,
110
+ method: request.method,
111
+ slot,
112
+ controller,
113
+ completion: completion.promise,
114
+ settle: completion.resolve,
115
+ inputs: new Map(),
116
+ inputSignal: deferred(),
117
+ timer: setTimeout(() => this.#cancelLegacyRound(id, new AppError('legacy_round_expired', 'Legacy input round expired', 504)), this.server.settings.maxTotalTimeoutMs),
118
+ detachSignal: () => undefined,
119
+ closed: false,
120
+ };
121
+ this.#legacyRounds.set(id, round);
122
+ slot.bridge.legacyRound = round;
123
+ this.#attachLegacyBridge(round, bridge);
124
+ if (round.closed)
125
+ return this.#awaitLegacyRound(round);
126
+ const options = this.#legacyRoundRequestOptions(request, slot.bridge, controller.signal);
127
+ void this.#requestOnSlot(slot, request, options).then((value) => round.settle({ kind: 'result', value }), (error) => round.settle({
128
+ kind: 'error',
129
+ error: error instanceof Error ? error : new Error(String(error)),
130
+ }));
131
+ return this.#awaitLegacyRound(round);
132
+ }
133
+ async #resumeLegacyRound(id, request, bridge) {
134
+ const round = this.#legacyRounds.get(id);
135
+ if (!round || round.closed) {
136
+ throw new AppError('legacy_round_not_found', 'Legacy input round expired or was restarted', 409);
137
+ }
138
+ if (round.method !== request.method || !contextIsModern(bridge.context)) {
139
+ throw new AppError('legacy_round_mismatch', 'Legacy input round does not match this request', 400);
140
+ }
141
+ const responses = request.params?.inputResponses;
142
+ if (isRecord(responses)) {
143
+ for (const [key, pending] of round.inputs) {
144
+ if (!Object.hasOwn(responses, key))
145
+ continue;
146
+ pending.resolve(bridge.transformClientResult?.(responses[key]) ?? responses[key]);
147
+ round.inputs.delete(key);
148
+ }
149
+ }
150
+ this.#attachLegacyBridge(round, bridge);
151
+ return this.#awaitLegacyRound(round);
152
+ }
153
+ async #awaitLegacyRound(round) {
154
+ while (!round.closed) {
155
+ if (round.inputs.size === 0) {
156
+ const outcome = await Promise.race([
157
+ round.completion,
158
+ round.inputSignal.promise.then(() => ({ kind: 'input' })),
159
+ ]);
160
+ if (outcome.kind === 'result') {
161
+ this.#finishLegacyRound(round);
162
+ return outcome.value;
163
+ }
164
+ if (outcome.kind === 'error') {
165
+ this.#finishLegacyRound(round, outcome.error);
166
+ throw outcome.error;
167
+ }
168
+ await Promise.resolve();
169
+ if (round.inputs.size === 0)
170
+ continue;
171
+ }
172
+ const inputRequests = Object.fromEntries([...round.inputs].map(([key, input]) => [key, input.request]));
173
+ round.inputSignal = deferred();
174
+ round.detachSignal();
175
+ round.slot.bridge.current = null;
176
+ return {
177
+ resultType: 'input_required',
178
+ inputRequests,
179
+ requestState: `${legacyRoundStatePrefix}${round.id}`,
180
+ };
181
+ }
182
+ throw new AppError('legacy_round_not_found', 'Legacy input round is closed', 409);
183
+ }
184
+ #attachLegacyBridge(round, bridge) {
185
+ round.detachSignal();
186
+ round.slot.bridge.current = bridge;
187
+ const signal = bridge.context.mcpReq.signal;
188
+ const onAbort = () => {
189
+ this.#cancelLegacyRound(round.id, new AppError('request_cancelled', 'Downstream request was cancelled', 499));
190
+ };
191
+ if (signal.aborted) {
192
+ onAbort();
193
+ return;
194
+ }
195
+ signal.addEventListener('abort', onAbort, { once: true });
196
+ round.detachSignal = () => signal.removeEventListener('abort', onAbort);
197
+ }
198
+ #cancelLegacyRound(id, error) {
199
+ const round = this.#legacyRounds.get(id);
200
+ if (!round || round.closed)
201
+ return;
202
+ round.controller.abort(error);
203
+ round.settle({ kind: 'error', error });
204
+ this.#finishLegacyRound(round, error);
205
+ }
206
+ #finishLegacyRound(round, error) {
207
+ if (round.closed)
208
+ return;
209
+ round.closed = true;
210
+ clearTimeout(round.timer);
211
+ round.detachSignal();
212
+ this.#legacyRounds.delete(round.id);
213
+ if (round.slot.bridge.legacyRound === round)
214
+ round.slot.bridge.legacyRound = null;
215
+ round.slot.bridge.current = null;
216
+ if (error) {
217
+ for (const pending of round.inputs.values())
218
+ pending.reject(error);
219
+ }
220
+ round.inputs.clear();
221
+ this.#release(round.slot);
222
+ }
223
+ #legacyRoundId(value) {
224
+ if (typeof value !== 'string' || !value.startsWith(legacyRoundStatePrefix))
225
+ return null;
226
+ const id = value.slice(legacyRoundStatePrefix.length);
227
+ return id.length === 0 ? null : id;
228
+ }
229
+ #legacyRoundRequestOptions(request, bridge, signal) {
230
+ const progressToken = this.#progressToken(request.params);
231
+ return {
232
+ signal,
233
+ timeout: this.server.settings.maxTotalTimeoutMs,
234
+ maxTotalTimeout: this.server.settings.maxTotalTimeoutMs,
235
+ resetTimeoutOnProgress: true,
236
+ ...(progressToken === null
237
+ ? {}
238
+ : {
239
+ onprogress: async (progress) => {
240
+ const active = bridge.current;
241
+ if (!active)
242
+ return;
243
+ await active.context.mcpReq.notify({
244
+ method: 'notifications/progress',
245
+ params: { progressToken, ...progress },
246
+ });
247
+ },
248
+ }),
249
+ };
250
+ }
251
+ async notify(notification, clientCapabilities, bridge = null) {
252
+ const slot = await this.#acquire(clientCapabilities);
253
+ slot.bridge.current = bridge;
254
+ try {
255
+ await slot.client.notification(notification);
256
+ }
257
+ finally {
258
+ slot.bridge.current = null;
259
+ this.#release(slot);
260
+ }
261
+ }
262
+ async subscribeResources(uris, clientCapabilities) {
263
+ const uniqueUris = [...new Set(uris)];
264
+ for (const uri of uniqueUris)
265
+ await this.#retainResource(clientCapabilities, uri);
266
+ let closed = false;
267
+ return async () => {
268
+ if (closed)
269
+ return;
270
+ closed = true;
271
+ for (const uri of uniqueUris)
272
+ await this.#releaseResource(clientCapabilities, uri);
273
+ };
274
+ }
275
+ async discoverSnapshot(previousVersion) {
276
+ const slot = await this.#acquire(discoveryCapabilities);
277
+ try {
278
+ const capabilities = slot.client.getServerCapabilities() ?? {};
279
+ const [toolsResult, resourcesResult, resourceTemplatesResult, promptsResult] = await Promise.all([
280
+ capabilities.tools
281
+ ? slot.client.listTools(undefined, { cacheMode: 'refresh' })
282
+ : Promise.resolve({ tools: [] }),
283
+ capabilities.resources
284
+ ? slot.client.listResources(undefined, { cacheMode: 'refresh' })
285
+ : Promise.resolve({ resources: [] }),
286
+ capabilities.resources
287
+ ? slot.client.listResourceTemplates(undefined, { cacheMode: 'refresh' })
288
+ : Promise.resolve({ resourceTemplates: [] }),
289
+ capabilities.prompts
290
+ ? slot.client.listPrompts(undefined, { cacheMode: 'refresh' })
291
+ : Promise.resolve({ prompts: [] }),
292
+ ]);
293
+ const protocolEra = slot.client.getProtocolEra();
294
+ if (!protocolEra)
295
+ throw new Error('Upstream protocol era is unavailable');
296
+ const body = {
297
+ serverInfo: slot.client.getServerVersion() ?? null,
298
+ capabilities,
299
+ instructions: slot.client.getInstructions() ?? null,
300
+ tools: toolsResult.tools,
301
+ resources: resourcesResult.resources,
302
+ resourceTemplates: resourceTemplatesResult.resourceTemplates,
303
+ prompts: promptsResult.prompts,
304
+ listResults: {
305
+ tools: toolsResult,
306
+ resources: resourcesResult,
307
+ resourceTemplates: resourceTemplatesResult,
308
+ prompts: promptsResult,
309
+ },
310
+ };
311
+ return {
312
+ serverId: this.server.id,
313
+ version: previousVersion + 1,
314
+ protocolVersion: slot.client.getNegotiatedProtocolVersion() ?? 'unknown',
315
+ protocolEra,
316
+ ...body,
317
+ fingerprint: fingerprint(body),
318
+ refreshedAt: new Date().toISOString(),
319
+ };
320
+ }
321
+ finally {
322
+ this.#release(slot);
323
+ }
324
+ }
325
+ restart() {
326
+ const current = this.#restartPromise;
327
+ if (current)
328
+ return current;
329
+ const operation = this.#restartNow();
330
+ this.#restartPromise = operation;
331
+ return operation.finally(() => {
332
+ if (this.#restartPromise === operation)
333
+ this.#restartPromise = null;
334
+ this.#wakeWaiters();
335
+ });
336
+ }
337
+ async #restartNow() {
338
+ const restartError = new AppError('upstream_restarted', 'Upstream connection was restarted', 503);
339
+ for (const id of [...this.#legacyRounds.keys()]) {
340
+ this.#cancelLegacyRound(id, restartError);
341
+ }
342
+ this.#generation += 1;
343
+ const slots = this.#slots.splice(0);
344
+ for (const slot of slots) {
345
+ slot.suppressCloseEvent = true;
346
+ slot.extensions.close();
347
+ }
348
+ const pendingConnections = [...this.#pendingConnections];
349
+ this.#pendingConnections.clear();
350
+ this.#wakeWaiters();
351
+ await Promise.allSettled([
352
+ ...slots.map((slot) => slot.client.close()),
353
+ ...pendingConnections.flatMap(({ client, transport }) => [client.close(), transport.close()]),
354
+ ]);
355
+ this.#resourceSubscriptions.clear();
356
+ this.#subscriptionCreates.clear();
357
+ }
358
+ async close() {
359
+ this.#closed = true;
360
+ await this.restart();
361
+ }
362
+ async #acquire(capabilities) {
363
+ await this.#restartPromise;
364
+ if (this.#closed)
365
+ throw new AppError('upstream_closed', 'Upstream adapter is closed', 503);
366
+ const generation = this.#generation;
367
+ const profile = stableJson(capabilities);
368
+ while (true) {
369
+ if (generation !== this.#generation || this.#closed) {
370
+ throw new AppError('upstream_restarted', 'Upstream connection was restarted', 503);
371
+ }
372
+ const idle = this.#slots.find((slot) => slot.profile === profile && !slot.busy);
373
+ if (idle) {
374
+ idle.busy = true;
375
+ return idle;
376
+ }
377
+ const creatingForProfile = this.#creating.get(profile) ?? 0;
378
+ let creatingTotal = 0;
379
+ for (const count of this.#creating.values())
380
+ creatingTotal += count;
381
+ if (this.#slots.length + creatingTotal < this.server.settings.maxConcurrency) {
382
+ this.#creating.set(profile, creatingForProfile + 1);
383
+ try {
384
+ const created = await this.#createConnection(capabilities, profile);
385
+ if (generation !== this.#generation || this.#closed) {
386
+ created.suppressCloseEvent = true;
387
+ created.extensions.close();
388
+ await created.client.close();
389
+ throw new AppError('upstream_restarted', 'Upstream connection was restarted', 503);
390
+ }
391
+ created.busy = true;
392
+ this.#slots.push(created);
393
+ return created;
394
+ }
395
+ finally {
396
+ const remaining = (this.#creating.get(profile) ?? 1) - 1;
397
+ if (remaining === 0)
398
+ this.#creating.delete(profile);
399
+ else
400
+ this.#creating.set(profile, remaining);
401
+ this.#wakeWaiters();
402
+ }
403
+ }
404
+ const recyclable = this.#slots.find((slot) => !slot.busy && slot.retained === 0);
405
+ if (recyclable) {
406
+ const index = this.#slots.indexOf(recyclable);
407
+ if (index >= 0)
408
+ this.#slots.splice(index, 1);
409
+ recyclable.suppressCloseEvent = true;
410
+ recyclable.extensions.close();
411
+ await recyclable.client.close().catch(() => undefined);
412
+ continue;
413
+ }
414
+ await new Promise((resolve) => this.#waiters.add(resolve));
415
+ }
416
+ }
417
+ #release(slot) {
418
+ slot.busy = false;
419
+ this.#wakeWaiters();
420
+ }
421
+ #wakeWaiters() {
422
+ for (const resolve of this.#waiters)
423
+ resolve();
424
+ this.#waiters.clear();
425
+ }
426
+ async #createConnection(capabilities, profile) {
427
+ const bridge = { current: null, legacyRound: null };
428
+ const connect = async (fallbackSse) => {
429
+ const client = this.#createClient(capabilities, bridge);
430
+ const transport = this.#createTransport(fallbackSse);
431
+ const generation = this.#generation;
432
+ let connected = false;
433
+ let slot = null;
434
+ client.onerror = (error) => {
435
+ this.#logger.warn('Upstream MCP client error', {
436
+ serverId: this.server.id,
437
+ error: error.message,
438
+ });
439
+ };
440
+ client.onclose = () => {
441
+ if (connected &&
442
+ !slot?.suppressCloseEvent &&
443
+ !this.#closed &&
444
+ generation === this.#generation) {
445
+ this.#events({ type: 'connection_closed', serverId: this.server.id });
446
+ }
447
+ };
448
+ if (transport instanceof StdioClientTransport) {
449
+ transport.stderr?.on('data', (chunk) => {
450
+ const message = this.#redactStderr(String(chunk).trim()).slice(0, 8_192);
451
+ if (message !== '')
452
+ this.#events({ type: 'stderr', serverId: this.server.id, message });
453
+ });
454
+ }
455
+ const pendingConnection = { client, transport };
456
+ this.#pendingConnections.add(pendingConnection);
457
+ try {
458
+ await client.connect(transport, { timeout: this.server.settings.connectTimeoutMs });
459
+ connected = true;
460
+ const protocolVersion = client.getNegotiatedProtocolVersion();
461
+ if (!protocolVersion)
462
+ throw new Error('Upstream protocol version is unavailable');
463
+ const extensions = new ExtensionTransportBridge(transport, protocolVersion, capabilities);
464
+ slot = {
465
+ client,
466
+ transport,
467
+ profile,
468
+ bridge,
469
+ busy: false,
470
+ retained: 0,
471
+ extensions,
472
+ suppressCloseEvent: false,
473
+ };
474
+ return slot;
475
+ }
476
+ catch (error) {
477
+ connected = false;
478
+ await client.close().catch(() => undefined);
479
+ throw error;
480
+ }
481
+ finally {
482
+ this.#pendingConnections.delete(pendingConnection);
483
+ }
484
+ };
485
+ try {
486
+ return await connect(false);
487
+ }
488
+ catch (error) {
489
+ if (this.server.transport.type !== 'streamable-http' ||
490
+ !this.server.transport.allowSseFallback) {
491
+ throw error;
492
+ }
493
+ this.#logger.warn('Streamable HTTP failed; trying legacy SSE', {
494
+ serverId: this.server.id,
495
+ error: errorMessage(error),
496
+ });
497
+ return connect(true);
498
+ }
499
+ }
500
+ #createClient(capabilities, bridge) {
501
+ const mode = this.server.transport.protocolMode === 'modern'
502
+ ? { pin: '2026-07-28' }
503
+ : this.server.transport.protocolMode;
504
+ const client = new Client({ name: 'toolhome', version: '0.1.0' }, {
505
+ capabilities,
506
+ versionNegotiation: { mode },
507
+ inputRequired: { autoFulfill: false },
508
+ listMaxPages: 256,
509
+ listChanged: {
510
+ tools: {
511
+ autoRefresh: false,
512
+ onChanged: (error) => this.#onListChanged(error, 'tools_changed'),
513
+ },
514
+ prompts: {
515
+ autoRefresh: false,
516
+ onChanged: (error) => this.#onListChanged(error, 'prompts_changed'),
517
+ },
518
+ resources: {
519
+ autoRefresh: false,
520
+ onChanged: (error) => this.#onListChanged(error, 'resources_changed'),
521
+ },
522
+ },
523
+ });
524
+ if (capabilities.roots) {
525
+ client.setRequestHandler('roots/list', async (request) => ListRootsResultSchema.parse(await this.#bridgeInputRequest(bridge, request)));
526
+ }
527
+ if (capabilities.sampling) {
528
+ client.setRequestHandler('sampling/createMessage', async (request) => samplingResultSchema.parse(await this.#bridgeInputRequest(bridge, request)));
529
+ }
530
+ if (capabilities.elicitation) {
531
+ client.setRequestHandler('elicitation/create', async (request) => ElicitResultSchema.parse(await this.#bridgeInputRequest(bridge, request)));
532
+ }
533
+ client.setNotificationHandler('notifications/resources/updated', async (notification) => {
534
+ this.#events({
535
+ type: 'resource_updated',
536
+ serverId: this.server.id,
537
+ uri: notification.params.uri,
538
+ });
539
+ const active = bridge.current;
540
+ if (active)
541
+ await active.context.mcpReq.notify(this.#bridgeNotification(active, notification));
542
+ });
543
+ client.setNotificationHandler('notifications/message', async (notification) => {
544
+ const active = bridge.current;
545
+ if (active)
546
+ await active.context.mcpReq.notify(this.#bridgeNotification(active, notification));
547
+ });
548
+ client.fallbackRequestHandler = async (request) => {
549
+ const active = this.#requireBridge(bridge);
550
+ const forwarded = active.transformRequest?.(request) ?? request;
551
+ const result = await active.context.mcpReq.send(forwarded, objectResultSchema, {
552
+ signal: active.context.mcpReq.signal,
553
+ timeout: this.server.settings.requestTimeoutMs,
554
+ maxTotalTimeout: this.server.settings.maxTotalTimeoutMs,
555
+ });
556
+ return objectResultSchema.parse(active.transformClientResult?.(result) ?? result);
557
+ };
558
+ client.fallbackNotificationHandler = async (notification) => {
559
+ const active = bridge.current;
560
+ if (!active)
561
+ return;
562
+ if (contextIsModern(active.context) &&
563
+ (notification.method === 'notifications/tasks/status' ||
564
+ notification.method === 'notifications/tasks')) {
565
+ return;
566
+ }
567
+ await active.context.mcpReq.notify(this.#bridgeNotification(active, notification));
568
+ };
569
+ return client;
570
+ }
571
+ #createTransport(fallbackSse) {
572
+ const credential = this.#credentials.resolve(this.server);
573
+ if (this.server.transport.type === 'stdio') {
574
+ const env = {
575
+ ...getDefaultEnvironment(),
576
+ ...this.server.transport.env,
577
+ ...credential.env,
578
+ };
579
+ return new StdioClientTransport({
580
+ command: this.server.transport.command,
581
+ args: this.server.transport.args,
582
+ env,
583
+ stderr: 'pipe',
584
+ ...(this.server.transport.cwd === undefined ? {} : { cwd: this.server.transport.cwd }),
585
+ });
586
+ }
587
+ const headers = {
588
+ ...this.server.transport.headers,
589
+ ...credential.headers,
590
+ };
591
+ const options = {
592
+ requestInit: { headers },
593
+ fetch: extensionFetch,
594
+ ...(credential.authProvider === undefined ? {} : { authProvider: credential.authProvider }),
595
+ };
596
+ return fallbackSse
597
+ ? new SSEClientTransport(new URL(this.server.transport.url), options)
598
+ : new StreamableHTTPClientTransport(new URL(this.server.transport.url), options);
599
+ }
600
+ #redactStderr(message) {
601
+ if (this.server.transport.type !== 'stdio')
602
+ return message;
603
+ const values = new Set(Object.values(this.server.transport.env));
604
+ try {
605
+ for (const value of Object.values(this.#credentials.resolve(this.server).env)) {
606
+ values.add(value);
607
+ }
608
+ }
609
+ catch {
610
+ return '[stderr redacted while credential state changed]';
611
+ }
612
+ let redacted = message;
613
+ for (const value of values) {
614
+ if (value === '')
615
+ continue;
616
+ redacted = redacted.replaceAll(value, '[REDACTED]');
617
+ }
618
+ return redacted;
619
+ }
620
+ #requestOptions(request, context) {
621
+ const progressToken = this.#progressToken(request.params);
622
+ return {
623
+ signal: context.mcpReq.signal,
624
+ timeout: this.server.settings.requestTimeoutMs,
625
+ maxTotalTimeout: this.server.settings.maxTotalTimeoutMs,
626
+ resetTimeoutOnProgress: true,
627
+ ...(progressToken === null
628
+ ? {}
629
+ : {
630
+ onprogress: async (progress) => {
631
+ await context.mcpReq.notify({
632
+ method: 'notifications/progress',
633
+ params: { progressToken, ...progress },
634
+ });
635
+ },
636
+ }),
637
+ };
638
+ }
639
+ #progressToken(params) {
640
+ if (!params)
641
+ return null;
642
+ const meta = params._meta;
643
+ if (typeof meta !== 'object' || meta === null)
644
+ return null;
645
+ const token = Reflect.get(meta, 'progressToken');
646
+ return typeof token === 'string' || typeof token === 'number' ? token : null;
647
+ }
648
+ #requestUri(request) {
649
+ const uri = request.params?.uri;
650
+ if (typeof uri !== 'string')
651
+ throw new AppError('invalid_resource_uri', 'Resource URI required');
652
+ return uri;
653
+ }
654
+ async #retainResource(capabilities, uri) {
655
+ const profile = stableJson(capabilities);
656
+ const key = `${profile}\0${uri}`;
657
+ const existing = this.#resourceSubscriptions.get(key);
658
+ if (existing) {
659
+ existing.references += 1;
660
+ return;
661
+ }
662
+ const pending = this.#subscriptionCreates.get(key);
663
+ if (pending) {
664
+ await pending;
665
+ const created = this.#resourceSubscriptions.get(key);
666
+ if (created)
667
+ created.references += 1;
668
+ return;
669
+ }
670
+ const operation = this.#createResourceSubscription(key, capabilities, uri);
671
+ this.#subscriptionCreates.set(key, operation);
672
+ try {
673
+ await operation;
674
+ }
675
+ finally {
676
+ if (this.#subscriptionCreates.get(key) === operation) {
677
+ this.#subscriptionCreates.delete(key);
678
+ }
679
+ }
680
+ }
681
+ async #createResourceSubscription(key, clientCapabilities, uri) {
682
+ const slot = await this.#acquire(clientCapabilities);
683
+ try {
684
+ const capabilities = slot.client.getServerCapabilities();
685
+ if (!capabilities?.resources?.subscribe)
686
+ return;
687
+ if (slot.client.getProtocolEra() === 'modern') {
688
+ const subscription = await slot.client.listen({ resourceSubscriptions: [uri] });
689
+ slot.retained += 1;
690
+ this.#resourceSubscriptions.set(key, { slot, subscription, references: 1 });
691
+ return;
692
+ }
693
+ await slot.client.subscribeResource({ uri });
694
+ slot.retained += 1;
695
+ this.#resourceSubscriptions.set(key, { slot, subscription: null, references: 1 });
696
+ }
697
+ finally {
698
+ this.#release(slot);
699
+ }
700
+ }
701
+ async #releaseResource(capabilities, uri) {
702
+ const key = `${stableJson(capabilities)}\0${uri}`;
703
+ await this.#subscriptionCreates.get(key)?.catch(() => undefined);
704
+ const state = this.#resourceSubscriptions.get(key);
705
+ if (!state)
706
+ return;
707
+ state.references -= 1;
708
+ if (state.references > 0)
709
+ return;
710
+ this.#resourceSubscriptions.delete(key);
711
+ if (!(await this.#acquireSpecific(state.slot)))
712
+ return;
713
+ try {
714
+ if (state.subscription)
715
+ await state.subscription.close();
716
+ else
717
+ await state.slot.client.unsubscribeResource({ uri });
718
+ }
719
+ finally {
720
+ state.slot.retained = Math.max(0, state.slot.retained - 1);
721
+ this.#release(state.slot);
722
+ }
723
+ }
724
+ async #acquireSpecific(slot) {
725
+ while (this.#slots.includes(slot)) {
726
+ if (!slot.busy) {
727
+ slot.busy = true;
728
+ return true;
729
+ }
730
+ await new Promise((resolve) => this.#waiters.add(resolve));
731
+ }
732
+ return false;
733
+ }
734
+ #requireBridge(bridge) {
735
+ if (!bridge.current) {
736
+ throw new AppError('client_capability_unavailable', 'The upstream requested a client capability outside an active MCP request', 502);
737
+ }
738
+ return bridge.current;
739
+ }
740
+ #bridgeNotification(active, notification) {
741
+ return active.transformNotification?.(notification) ?? notification;
742
+ }
743
+ #bridgeInputRequest(bridge, request) {
744
+ const active = this.#requireBridge(bridge);
745
+ const forwarded = active.transformRequest?.(request) ?? request;
746
+ const round = bridge.legacyRound;
747
+ if (!round || round.closed) {
748
+ return active.context.mcpReq
749
+ .send(forwarded, objectResultSchema, {
750
+ signal: active.context.mcpReq.signal,
751
+ timeout: this.server.settings.requestTimeoutMs,
752
+ maxTotalTimeout: this.server.settings.maxTotalTimeoutMs,
753
+ })
754
+ .then((result) => active.transformClientResult?.(result) ?? result);
755
+ }
756
+ const key = `input-${randomUUID()}`;
757
+ return new Promise((resolve, reject) => {
758
+ round.inputs.set(key, { request: forwarded, resolve, reject });
759
+ round.inputSignal.resolve(undefined);
760
+ });
761
+ }
762
+ #onListChanged(error, type) {
763
+ if (error) {
764
+ this.#logger.warn('Upstream list change refresh failed', {
765
+ serverId: this.server.id,
766
+ error: toError(error).message,
767
+ });
768
+ return;
769
+ }
770
+ this.#events({ type, serverId: this.server.id });
771
+ }
772
+ }
773
+ function deferred() {
774
+ let settled = false;
775
+ let resolveValue = (value) => {
776
+ void value;
777
+ };
778
+ let rejectValue = (error) => {
779
+ void error;
780
+ };
781
+ const promise = new Promise((resolve, reject) => {
782
+ resolveValue = resolve;
783
+ rejectValue = reject;
784
+ });
785
+ return {
786
+ promise,
787
+ resolve(value) {
788
+ if (settled)
789
+ return;
790
+ settled = true;
791
+ resolveValue(value);
792
+ },
793
+ reject(error) {
794
+ if (settled)
795
+ return;
796
+ settled = true;
797
+ rejectValue(error);
798
+ },
799
+ };
800
+ }
801
+ function contextIsModern(context) {
802
+ return context.mcpReq.envelope !== undefined;
803
+ }
804
+ function isRecord(value) {
805
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
806
+ }
807
+ //# sourceMappingURL=adapter.js.map