@zackbart/connecta 0.8.0 → 0.9.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 (151) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +31 -55
  3. package/SECURITY.md +5 -11
  4. package/dist/activity.d.ts +0 -8
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js +0 -1
  7. package/dist/activity.js.map +1 -1
  8. package/dist/auth/bearer.d.ts +1 -7
  9. package/dist/auth/bearer.d.ts.map +1 -1
  10. package/dist/auth/bearer.js +2 -23
  11. package/dist/auth/bearer.js.map +1 -1
  12. package/dist/auth/clerk.d.ts +2 -11
  13. package/dist/auth/clerk.d.ts.map +1 -1
  14. package/dist/auth/clerk.js +31 -46
  15. package/dist/auth/clerk.js.map +1 -1
  16. package/dist/auth/downstream-oauth.d.ts +15 -6
  17. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  18. package/dist/auth/downstream-oauth.js +60 -11
  19. package/dist/auth/downstream-oauth.js.map +1 -1
  20. package/dist/call-admission.d.ts.map +1 -1
  21. package/dist/call-admission.js +0 -1
  22. package/dist/call-admission.js.map +1 -1
  23. package/dist/catalog-service.d.ts +1 -2
  24. package/dist/catalog-service.d.ts.map +1 -1
  25. package/dist/catalog-service.js +12 -18
  26. package/dist/catalog-service.js.map +1 -1
  27. package/dist/catalog.d.ts +6 -0
  28. package/dist/catalog.d.ts.map +1 -1
  29. package/dist/catalog.js +51 -0
  30. package/dist/catalog.js.map +1 -1
  31. package/dist/connectors/remote-mcp.d.ts +1 -1
  32. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  33. package/dist/connectors/remote-mcp.js +73 -71
  34. package/dist/connectors/remote-mcp.js.map +1 -1
  35. package/dist/errors.d.ts +19 -1
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +1 -0
  38. package/dist/errors.js.map +1 -1
  39. package/dist/execute.d.ts +4 -9
  40. package/dist/execute.d.ts.map +1 -1
  41. package/dist/execute.js +41 -20
  42. package/dist/execute.js.map +1 -1
  43. package/dist/executor-result.js +1 -1
  44. package/dist/executor-result.js.map +1 -1
  45. package/dist/index.d.ts +3 -82
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +17 -90
  48. package/dist/index.js.map +1 -1
  49. package/dist/invocation.d.ts +5 -0
  50. package/dist/invocation.d.ts.map +1 -1
  51. package/dist/invocation.js +46 -10
  52. package/dist/invocation.js.map +1 -1
  53. package/dist/meta-tools.d.ts +4 -12
  54. package/dist/meta-tools.d.ts.map +1 -1
  55. package/dist/meta-tools.js +119 -169
  56. package/dist/meta-tools.js.map +1 -1
  57. package/dist/operator-ui/generated.d.ts +2 -2
  58. package/dist/operator-ui/generated.d.ts.map +1 -1
  59. package/dist/operator-ui/generated.js +2 -2
  60. package/dist/operator-ui/generated.js.map +1 -1
  61. package/dist/operator-ui/model.d.ts +1 -22
  62. package/dist/operator-ui/model.d.ts.map +1 -1
  63. package/dist/operator-ui/model.js.map +1 -1
  64. package/dist/registry.d.ts +11 -178
  65. package/dist/registry.d.ts.map +1 -1
  66. package/dist/registry.js +52 -330
  67. package/dist/registry.js.map +1 -1
  68. package/dist/retired-toolkits.d.ts +3 -0
  69. package/dist/retired-toolkits.d.ts.map +1 -0
  70. package/dist/retired-toolkits.js +9 -0
  71. package/dist/retired-toolkits.js.map +1 -0
  72. package/dist/routes/activity.d.ts.map +1 -1
  73. package/dist/routes/activity.js +2 -5
  74. package/dist/routes/activity.js.map +1 -1
  75. package/dist/routes/credentials.d.ts.map +1 -1
  76. package/dist/routes/credentials.js +1 -12
  77. package/dist/routes/credentials.js.map +1 -1
  78. package/dist/routes/mcp.d.ts.map +1 -1
  79. package/dist/routes/mcp.js +94 -188
  80. package/dist/routes/mcp.js.map +1 -1
  81. package/dist/routes/oauth.d.ts.map +1 -1
  82. package/dist/routes/oauth.js +3 -8
  83. package/dist/routes/oauth.js.map +1 -1
  84. package/dist/routes/shared.d.ts +9 -30
  85. package/dist/routes/shared.d.ts.map +1 -1
  86. package/dist/routes/shared.js +10 -69
  87. package/dist/routes/shared.js.map +1 -1
  88. package/dist/routes/ui.d.ts.map +1 -1
  89. package/dist/routes/ui.js +4 -10
  90. package/dist/routes/ui.js.map +1 -1
  91. package/dist/server.d.ts.map +1 -1
  92. package/dist/server.js +1 -31
  93. package/dist/server.js.map +1 -1
  94. package/dist/skills.d.ts +3 -6
  95. package/dist/skills.d.ts.map +1 -1
  96. package/dist/skills.js +9 -38
  97. package/dist/skills.js.map +1 -1
  98. package/dist/timeout.d.ts.map +1 -1
  99. package/dist/timeout.js +2 -4
  100. package/dist/timeout.js.map +1 -1
  101. package/dist/types.d.ts +8 -61
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/ui.d.ts +1 -2
  104. package/dist/ui.d.ts.map +1 -1
  105. package/dist/ui.js +5 -35
  106. package/dist/ui.js.map +1 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/package.json +3 -2
  110. package/src/activity.ts +0 -9
  111. package/src/auth/bearer.ts +3 -32
  112. package/src/auth/clerk.ts +35 -48
  113. package/src/auth/downstream-oauth.ts +106 -24
  114. package/src/call-admission.ts +0 -1
  115. package/src/catalog-service.ts +23 -23
  116. package/src/catalog.ts +55 -0
  117. package/src/connectors/remote-mcp.ts +106 -86
  118. package/src/errors.ts +22 -0
  119. package/src/execute.ts +43 -24
  120. package/src/executor-result.ts +1 -1
  121. package/src/index.ts +24 -208
  122. package/src/invocation.ts +63 -10
  123. package/src/meta-tools.ts +139 -187
  124. package/src/operator-ui/browser.css +1 -54
  125. package/src/operator-ui/browser.ts +16 -150
  126. package/src/operator-ui/generated.ts +2 -2
  127. package/src/operator-ui/model.ts +1 -23
  128. package/src/registry.ts +59 -429
  129. package/src/retired-toolkits.ts +14 -0
  130. package/src/routes/activity.ts +1 -6
  131. package/src/routes/credentials.ts +0 -12
  132. package/src/routes/mcp.ts +110 -230
  133. package/src/routes/oauth.ts +2 -8
  134. package/src/routes/shared.ts +10 -100
  135. package/src/routes/ui.ts +2 -12
  136. package/src/server.ts +1 -31
  137. package/src/skills.ts +9 -38
  138. package/src/timeout.ts +2 -4
  139. package/src/types.ts +11 -62
  140. package/src/ui.ts +9 -46
  141. package/src/version.ts +1 -1
  142. package/dist/credential-health.d.ts +0 -205
  143. package/dist/credential-health.d.ts.map +0 -1
  144. package/dist/credential-health.js +0 -610
  145. package/dist/credential-health.js.map +0 -1
  146. package/dist/toolkits.d.ts +0 -124
  147. package/dist/toolkits.d.ts.map +0 -1
  148. package/dist/toolkits.js +0 -322
  149. package/dist/toolkits.js.map +0 -1
  150. package/src/credential-health.ts +0 -835
  151. package/src/toolkits.ts +0 -461
package/src/routes/mcp.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
1
+ import {
2
+ createMcpHandler,
3
+ isLegacyRequest,
4
+ McpServer,
5
+ WebStandardStreamableHTTPServerTransport,
6
+ } from "@modelcontextprotocol/server";
3
7
  import type { ActivityActor, ActivityRequestContext } from "../activity.js";
4
8
  import { registerExecuteTool } from "../execute.js";
5
9
  import {
@@ -7,14 +11,11 @@ import {
7
11
  type AdmissionLease,
8
12
  } from "../executor-admission.js";
9
13
  import { registerMetaTools } from "../meta-tools.js";
10
- import { ScopedRegistry, type Registry, type RegistryView } from "../registry.js";
14
+ import type { RegistryView } from "../registry.js";
11
15
  import { CONNECTA_INSTRUCTIONS } from "../skills.js";
12
- import { TOOLKIT_NAME_RE, type Toolkit } from "../toolkits.js";
13
- import type { Logger, ToolkitBinding } from "../types.js";
16
+ import type { Logger } from "../types.js";
14
17
  import {
15
- MAX_ECHOED_TOOLKIT_NAME,
16
18
  authorize,
17
- loggableValue,
18
19
  type RouteContext,
19
20
  type RuntimeExecutionContext,
20
21
  type ServerOptions,
@@ -24,7 +25,7 @@ export const MCP_CORS_HEADERS = {
24
25
  "Access-Control-Allow-Origin": "*",
25
26
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
26
27
  "Access-Control-Allow-Headers":
27
- "Content-Type, Authorization, mcp-protocol-version, mcp-session-id",
28
+ "Content-Type, Authorization, mcp-protocol-version, mcp-session-id, mcp-method, mcp-name",
28
29
  };
29
30
 
30
31
  // Browser-based MCP clients call /mcp cross-origin. Without CORS on every
@@ -142,167 +143,45 @@ function releaseAdmissionWithResponse(
142
143
  });
143
144
  }
144
145
 
145
- /** What one MCP connection may see: the full registry, or one toolkit's view. */
146
- interface McpScope {
147
- registry: RegistryView;
148
- /** Set only under `?toolkit=`; recorded on activity events. */
149
- toolkitId?: string;
150
- }
151
-
152
- /** The one refusal a bound identity ever sees. Constant on purpose — see below. */
153
- const TOOLKIT_FORBIDDEN_BODY = JSON.stringify({
154
- jsonrpc: "2.0",
155
- id: null,
156
- error: {
157
- code: -32600,
158
- message:
159
- "Not permitted to use the requested toolkit. This credential is bound " +
160
- "to a specific toolkit — check the ?toolkit= value in this deployment's " +
161
- "MCP endpoint URL with the operator.",
162
- },
163
- });
164
-
165
146
  /**
166
- * 403 for every binding refusal, with a body that does not depend on WHY.
167
- *
168
- * A bound identity asking for a toolkit it may not open, for a toolkit that does
169
- * not exist, or for no toolkit at all gets byte-identical responses, so a team
170
- * credential cannot be used to enumerate the org's other teams — the boundary
171
- * would leak the very structure it exists to hide. The operator log below is
172
- * where the three cases are told apart.
173
- */
174
- function toolkitForbidden(): Response {
175
- return new Response(TOOLKIT_FORBIDDEN_BODY, {
176
- status: 403,
177
- headers: {
178
- "Content-Type": "application/json",
179
- "Cache-Control": "no-store",
180
- },
181
- });
182
- }
183
-
184
- /** How a rejected connection is named in the operator log. */
185
- function identityLabel(actor: ActivityActor): string {
186
- return actor.id ? `${actor.kind} ${loggableValue(actor.id)}` : actor.kind;
187
- }
188
-
189
- /**
190
- * Resolve `?toolkit=<name>` into the registry view this connection may see,
191
- * enforcing the caller's toolkit binding (documentation/toolkits.md) on the way.
192
- *
193
- * For an UNBOUND identity (no binding configured — the pre-#37 shape):
194
- *
195
- * - absent → the full registry, byte-identical to a deployment with no toolkits
196
- * - known → a `ScopedRegistry` over that toolkit (the one visibility boundary)
197
- * - anything else, including `?toolkit=` with an empty value → an explicit
198
- * 404. Never a silent fallback to the full registry.
199
- *
200
- * For a BOUND identity, membership is checked FIRST and refusal is a flat 403:
201
- * a toolkit outside the binding, an unknown name, and (without `unscoped`) an
202
- * omitted `?toolkit=` are all refused before any `ScopedRegistry` is built, and
203
- * all three produce the same response.
204
- *
205
- * Neither error enumerates the configured toolkits: the name selects a scope, so
206
- * a wrong guess gets a flat refusal, not a directory.
147
+ * 404 for any `?toolkit=` value, kept after the feature's retirement (#178).
207
148
  *
208
- * Because of that and because SDK clients treat a 404/403 on the transport
209
- * endpoint as a transport failure and discard the body — every rejection is also
210
- * logged operator-side (issue #47), which is the channel that actually reaches a
211
- * human. The log line may name the configured or bound toolkits; the response
212
- * still may not.
149
+ * Toolkits are gone, but the URLs that named them are not: clients were handed
150
+ * MCP endpoint URLs with `?toolkit=` baked in, and nothing about upgrading the
151
+ * server rotates them. Silently serving those clients the full registry would
152
+ * turn a former scoping boundary into fail-open so a request still sending
153
+ * the param gets the same explicit 404 an unknown toolkit got before, plus an
154
+ * operator-side log line, which (as ever — issue #47) is the channel that
155
+ * actually reaches a human.
213
156
  */
214
- function resolveToolkitScope(
215
- url: URL,
216
- registry: Registry,
217
- toolkits: ReadonlyMap<string, Toolkit> | undefined,
218
- logger: Logger,
219
- identity: { actor: ActivityActor; binding?: ToolkitBinding },
220
- ):
221
- | { ok: true; scope: McpScope }
222
- | { ok: false; response: Response } {
223
- const requested = url.searchParams.get("toolkit");
224
- const binding = identity.binding;
225
- const scopeFor = (toolkit: Toolkit) => ({
226
- ok: true as const,
227
- scope: {
228
- registry: new ScopedRegistry(registry, toolkit),
229
- toolkitId: toolkit.name,
230
- },
231
- });
232
-
233
- if (binding) {
234
- const who = identityLabel(identity.actor);
235
- const bound = `Bound toolkits: ${binding.toolkits.join(", ") || "(none)"}${
236
- binding.unscoped ? ", plus unscoped access" : ""
237
- }.`;
238
- if (requested === null) {
239
- if (binding.unscoped) return { ok: true, scope: { registry } };
240
- logger.warn(
241
- `[connecta] refused an unscoped /mcp connection from ${who} with 403: ` +
242
- "its toolkit binding does not allow the full registry. " +
243
- bound +
244
- " The client sees a transport-level failure and never the reason, so " +
245
- "give it an MCP endpoint URL with a ?toolkit= value it is bound to.",
246
- );
247
- return { ok: false, response: toolkitForbidden() };
248
- }
249
- const permitted = binding.toolkits.includes(requested);
250
- const toolkit = permitted ? toolkits?.get(requested) : undefined;
251
- if (toolkit) return scopeFor(toolkit);
252
- logger.warn(
253
- `[connecta] refused an /mcp connection from ${who} with 403: it asked ` +
254
- `for toolkit ${loggableValue(requested)}, which ` +
255
- (permitted
256
- ? "its binding allows but this deployment does not configure"
257
- : "its toolkit binding does not include") +
258
- ". " +
259
- bound +
260
- " The client sees a transport-level failure and never the reason, so " +
261
- "check the ?toolkit= value in its MCP endpoint URL.",
262
- );
263
- return { ok: false, response: toolkitForbidden() };
264
- }
265
-
266
- if (requested === null) return { ok: true, scope: { registry } };
267
- const toolkit = toolkits?.get(requested);
268
- if (toolkit) return scopeFor(toolkit);
269
- const configured = toolkits && toolkits.size > 0 ? [...toolkits.keys()] : [];
157
+ function toolkitRetired(logger: Logger): Response {
270
158
  logger.warn(
271
- "[connecta] rejected an /mcp connection asking for unknown toolkit " +
272
- `${loggableValue(requested)} with 404. ` +
273
- (configured.length > 0
274
- ? `Configured toolkits: ${configured.join(", ")}.`
275
- : "This deployment configures no toolkits, so no ?toolkit= value is accepted.") +
276
- " The client sees a transport-level failure and never the reason, so " +
277
- "check the ?toolkit= value in its MCP endpoint URL.",
159
+ "[connecta] rejected an /mcp connection carrying ?toolkit= with 404: " +
160
+ "toolkits were retired in issue #178 (see ethos.md) one deployment " +
161
+ "serves one audience. The client sees a transport-level failure and " +
162
+ "never the reason, so remove the ?toolkit= value from its MCP endpoint " +
163
+ "URL, or point it at the deployment for its audience.",
278
164
  );
279
- const label =
280
- requested.length <= MAX_ECHOED_TOOLKIT_NAME &&
281
- TOOLKIT_NAME_RE.test(requested)
282
- ? `"${requested}"`
283
- : "requested";
284
- return {
285
- ok: false,
286
- response: new Response(
287
- JSON.stringify({
288
- jsonrpc: "2.0",
289
- id: null,
290
- error: {
291
- code: -32600,
292
- message:
293
- `Unknown toolkit ${label}. Check the ?toolkit= value in this ` +
294
- "deployment's MCP endpoint URL with the operator.",
295
- },
296
- }),
297
- {
298
- status: 404,
299
- headers: {
300
- "Content-Type": "application/json",
301
- "Cache-Control": "no-store",
302
- },
165
+ return new Response(
166
+ JSON.stringify({
167
+ jsonrpc: "2.0",
168
+ id: null,
169
+ error: {
170
+ code: -32600,
171
+ message:
172
+ "This deployment does not accept ?toolkit=. Toolkits were retired " +
173
+ "in issue #178 — remove the ?toolkit= value from the MCP endpoint " +
174
+ "URL, or ask the operator for the deployment serving this audience.",
303
175
  },
304
- ),
305
- };
176
+ }),
177
+ {
178
+ status: 404,
179
+ headers: {
180
+ "Content-Type": "application/json",
181
+ "Cache-Control": "no-store",
182
+ },
183
+ },
184
+ );
306
185
  }
307
186
 
308
187
  async function serveMcp(
@@ -310,61 +189,79 @@ async function serveMcp(
310
189
  opts: ServerOptions,
311
190
  baseUrl: string,
312
191
  actor: ActivityActor,
313
- scope: McpScope,
192
+ registry: RegistryView,
314
193
  runtimeContext?: RuntimeExecutionContext,
315
194
  ): Promise<Response> {
316
- // Fresh McpServer + transport per request (SDK ≥1.26 requirement), stateless.
317
- const server = new McpServer(opts.serverInfo, {
318
- instructions: CONNECTA_INSTRUCTIONS,
319
- });
320
- const activity: ActivityRequestContext | undefined = opts.activity
321
- ? {
322
- sink: opts.activity,
323
- actor,
324
- requestId: crypto.randomUUID(),
325
- serverInfo: opts.serverInfo,
326
- ...(opts.activityDeploymentId
327
- ? { deploymentId: opts.activityDeploymentId }
328
- : {}),
329
- ...(scope.toolkitId ? { toolkitId: scope.toolkitId } : {}),
330
- ...(runtimeContext?.waitUntil
331
- ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
332
- : {}),
333
- logger: opts.logger,
334
- }
335
- : undefined;
336
- // `scope.registry` is the connection's VIEW — the full registry, or one
337
- // toolkit's ScopedRegistry. Nothing below may reach for `opts.registry`.
338
- const registry = scope.registry;
339
- registerMetaTools(server, registry, {
340
- baseUrl,
341
- ...(activity ? { activity } : {}),
342
- ...(opts.defaultToolTimeoutMs !== undefined
343
- ? { defaultToolTimeoutMs: opts.defaultToolTimeoutMs }
344
- : {}),
345
- ...(opts.probeTimeoutMs !== undefined
346
- ? { probeTimeoutMs: opts.probeTimeoutMs }
347
- : {}),
348
- ...(opts.discoveryConcurrency !== undefined
349
- ? { discoveryConcurrency: opts.discoveryConcurrency }
350
- : {}),
351
- requestSignal: request.signal,
352
- ...(runtimeContext
353
- ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
354
- : {}),
355
- });
356
- if (opts.executor) {
357
- registerExecuteTool(server, registry, {
195
+ const createServer = (): McpServer => {
196
+ const server = new McpServer(opts.serverInfo, {
197
+ instructions: CONNECTA_INSTRUCTIONS,
198
+ cacheHints: {
199
+ "tools/list": {
200
+ ttlMs: 3_600_000,
201
+ cacheScope: "private",
202
+ },
203
+ },
204
+ });
205
+ const activity: ActivityRequestContext | undefined = opts.activity
206
+ ? {
207
+ sink: opts.activity,
208
+ actor,
209
+ requestId: crypto.randomUUID(),
210
+ serverInfo: opts.serverInfo,
211
+ ...(opts.activityDeploymentId
212
+ ? { deploymentId: opts.activityDeploymentId }
213
+ : {}),
214
+ ...(runtimeContext?.waitUntil
215
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
216
+ : {}),
217
+ logger: opts.logger,
218
+ }
219
+ : undefined;
220
+ registerMetaTools(server, registry, {
358
221
  baseUrl,
359
- executor: opts.executor,
360
- logger: opts.logger,
361
222
  ...(activity ? { activity } : {}),
362
- requestSignal: request.signal,
223
+ ...(opts.defaultToolTimeoutMs !== undefined
224
+ ? { defaultToolTimeoutMs: opts.defaultToolTimeoutMs }
225
+ : {}),
226
+ ...(opts.probeTimeoutMs !== undefined
227
+ ? { probeTimeoutMs: opts.probeTimeoutMs }
228
+ : {}),
363
229
  ...(opts.discoveryConcurrency !== undefined
364
230
  ? { discoveryConcurrency: opts.discoveryConcurrency }
365
231
  : {}),
232
+ requestSignal: request.signal,
233
+ ...(runtimeContext
234
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
235
+ : {}),
366
236
  });
237
+ if (opts.executor) {
238
+ registerExecuteTool(server, registry, {
239
+ baseUrl,
240
+ executor: opts.executor,
241
+ logger: opts.logger,
242
+ ...(activity ? { activity } : {}),
243
+ requestSignal: request.signal,
244
+ ...(opts.discoveryConcurrency !== undefined
245
+ ? { discoveryConcurrency: opts.discoveryConcurrency }
246
+ : {}),
247
+ });
248
+ }
249
+ return server;
250
+ };
251
+
252
+ // The v2 entry's built-in legacy fallback streams 2025 results as SSE.
253
+ // Connecta's established wire contract is JSON, so retain the documented
254
+ // user-land legacy branch with the same transport setting while the modern
255
+ // branch uses the fetch-native handler.
256
+ if (!(await isLegacyRequest(request))) {
257
+ return createMcpHandler(createServer, {
258
+ legacy: "reject",
259
+ onerror: (error) => opts.logger.error("[connecta] MCP handler error", error),
260
+ }).fetch(request);
367
261
  }
262
+
263
+ // Fresh server + transport per legacy request, stateless and JSON-shaped.
264
+ const server = createServer();
368
265
  const transport = new WebStandardStreamableHTTPServerTransport({
369
266
  enableJsonResponse: true,
370
267
  });
@@ -399,10 +296,8 @@ export function createMcpRoute(
399
296
  const {
400
297
  path,
401
298
  request,
402
- url,
403
299
  baseUrl,
404
300
  runtimeContext,
405
- sweepCredentials,
406
301
  } = context;
407
302
  if (path !== "/mcp") return null;
408
303
  let admission: AdmissionLease;
@@ -433,9 +328,7 @@ export function createMcpRoute(
433
328
  throw error;
434
329
  }
435
330
  try {
436
- // Authenticate BEFORE resolving ?toolkit=: an unauthenticated caller
437
- // must not be able to probe which toolkit names exist.
438
- const authz = await authorize(request, baseUrl, opts.auth, opts.logger);
331
+ const authz = await authorize(request, baseUrl, opts.auth);
439
332
  if (!authz.ok) {
440
333
  return releaseAdmissionWithResponse(
441
334
  withMcpCors(authz.response),
@@ -443,26 +336,13 @@ export function createMcpRoute(
443
336
  request.signal,
444
337
  );
445
338
  }
446
- const selected = resolveToolkitScope(
447
- url,
448
- opts.registry,
449
- opts.toolkits,
450
- opts.logger,
451
- {
452
- actor: authz.actor,
453
- ...(authz.toolkitBinding
454
- ? { binding: authz.toolkitBinding }
455
- : {}),
456
- },
457
- );
458
- if (!selected.ok) {
339
+ if (new URL(request.url).searchParams.has("toolkit")) {
459
340
  return releaseAdmissionWithResponse(
460
- withMcpCors(selected.response),
341
+ withMcpCors(toolkitRetired(opts.logger)),
461
342
  admission,
462
343
  request.signal,
463
344
  );
464
345
  }
465
- sweepCredentials();
466
346
  return releaseAdmissionWithResponse(
467
347
  withMcpCors(
468
348
  await serveMcp(
@@ -470,7 +350,7 @@ export function createMcpRoute(
470
350
  opts,
471
351
  baseUrl,
472
352
  authz.actor,
473
- selected.scope,
353
+ opts.registry,
474
354
  runtimeContext,
475
355
  ),
476
356
  ),
@@ -30,7 +30,6 @@ async function handleOAuthManagementRequest(
30
30
  request,
31
31
  baseUrl,
32
32
  opts.auth,
33
- opts.logger,
34
33
  "OAuth management",
35
34
  );
36
35
  if (!admin.ok) return admin.response;
@@ -61,11 +60,10 @@ async function handleOAuthManagementRequest(
61
60
  operationError = error;
62
61
  }
63
62
 
64
- // The old grant and its catalog verdict are invalid after either operation,
63
+ // The old grant and its cached catalog are invalid after either operation,
65
64
  // including a partially failed physical cleanup whose epoch fence succeeded.
66
65
  try {
67
66
  await opts.registry.invalidateStored(connectorId);
68
- await opts.registry.clearCredentialHealth(connectorId);
69
67
  } catch (error) {
70
68
  operationError ??= error;
71
69
  }
@@ -311,12 +309,8 @@ export async function routeOAuthCallback(
311
309
  return refused();
312
310
  }
313
311
  try {
314
- await connector.finishAuth(code, connectorContext);
312
+ await connector.finishAuth(code, connectorContext, url.searchParams);
315
313
  await opts.registry.invalidateStored(id);
316
- // Recovery, without a restart: the grant this connector was reported dead
317
- // for has just been replaced, so drop the verdict rather than let a stale
318
- // `auth_required` survive until the next scheduled check.
319
- await opts.registry.clearCredentialHealth(id);
320
314
  return html(
321
315
  `Connected "${id}". You can close this window.`,
322
316
  200,
@@ -1,16 +1,14 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import type { Implementation } from "@modelcontextprotocol/server";
2
2
  import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity.js";
3
3
  import type { CredentialVault } from "../credentials.js";
4
4
  import type { DeferredWork } from "../connector-scope.js";
5
5
  import type { AdmissionController } from "../executor-admission.js";
6
6
  import type { Registry } from "../registry.js";
7
- import { resolveIdentityBinding, type Toolkit } from "../toolkits.js";
8
7
  import type {
9
8
  ConnectaBranding,
10
9
  Executor,
11
10
  InboundAuth,
12
11
  Logger,
13
- ToolkitBinding,
14
12
  } from "../types.js";
15
13
  import { operatorPageForPath } from "../ui.js";
16
14
 
@@ -20,7 +18,7 @@ export interface ServerOptions {
20
18
  publicUrl?: string;
21
19
  // The SDK's Implementation shape: name/version plus optional title,
22
20
  // websiteUrl, and icons (MCP icons spec) that clients may render.
23
- serverInfo: ConstructorParameters<typeof McpServer>[0];
21
+ serverInfo: Implementation;
24
22
  logger: Logger;
25
23
  activity?: ActivityStore;
26
24
  activityReadGate?: ActivityReadGate;
@@ -40,11 +38,6 @@ export interface ServerOptions {
40
38
  credentialVault?: CredentialVault;
41
39
  /** Optional browser UI and OAuth result-page labels. */
42
40
  branding?: ConnectaBranding;
43
- /**
44
- * Validated named scopes, selected per connection with `?toolkit=<name>` on
45
- * `/mcp`. Omit (or leave empty) and every connection sees the full registry.
46
- */
47
- toolkits?: ReadonlyMap<string, Toolkit>;
48
41
  }
49
42
 
50
43
  export interface RuntimeExecutionContext {
@@ -59,7 +52,6 @@ export interface RouteContext {
59
52
  opts: ServerOptions;
60
53
  defer: DeferredWork | undefined;
61
54
  runtimeContext: RuntimeExecutionContext | undefined;
62
- sweepCredentials(): void;
63
55
  }
64
56
 
65
57
  export function msg(err: unknown): string {
@@ -80,13 +72,13 @@ export function privateJson(
80
72
  const MAX_LOGGED_VALUE_LENGTH = 64;
81
73
 
82
74
  /**
83
- * Bounded, escaped form of a caller-influenced value (a rejected toolkit name,
84
- * identity id, or OAuth callback connector id) for the operator log. Goes
75
+ * Bounded, escaped form of a caller-influenced value (an identity id or OAuth
76
+ * callback connector id) for the operator log. Goes
85
77
  * through JSON.stringify so a caller-controlled newline or control character
86
78
  * cannot forge a log line, plus a hand-rolled escape for U+2028/U+2029, which
87
79
  * JSON.stringify leaves raw even though a log reader treats them as line
88
- * terminators. Truncated to a small shared cap (also the toolkit response's echo
89
- * limit), so an oversized value cannot flood the log either.
80
+ * terminators. Truncated to a small shared cap so an oversized value cannot
81
+ * flood the log either.
90
82
  */
91
83
  export function loggableValue(requested: string): string {
92
84
  const bounded = requested.slice(0, MAX_LOGGED_VALUE_LENGTH);
@@ -97,9 +89,6 @@ export function loggableValue(requested: string): string {
97
89
  return escaped + (bounded.length < requested.length ? " (truncated)" : "");
98
90
  }
99
91
 
100
- /** Length beyond which a rejected toolkit name is not echoed back. */
101
- export const MAX_ECHOED_TOOLKIT_NAME = MAX_LOGGED_VALUE_LENGTH;
102
-
103
92
  const ACTIVITY_ACTOR_NAMESPACE_RE = /^[\x21-\x7e]{1,256}$/;
104
93
 
105
94
  export function activityActorNamespace(
@@ -111,29 +100,16 @@ export function activityActorNamespace(
111
100
  : undefined;
112
101
  }
113
102
 
114
- /**
115
- * Refusal for an identity whose toolkit binding cannot be trusted — a malformed
116
- * declaration, or a malformed per-identity binding out of `authorize`. The
117
- * caller is authenticated, so this is a 403, and it is deliberately opaque: the
118
- * cause is an operator bug, and the operator reads it in the log, not the client.
119
- */
120
- function unusableBinding(): Response {
121
- return privateJson({ error: "forbidden" }, { status: 403 });
122
- }
123
-
124
103
  export async function authorize(
125
104
  request: Request,
126
105
  baseUrl: string,
127
106
  auth: InboundAuth[],
128
- logger: Logger,
129
107
  ): Promise<
130
108
  | {
131
109
  ok: true;
132
110
  actor: ActivityActor;
133
111
  /** True only when the admitting provider can also authorize UI mutation. */
134
112
  uiAdminEligible?: boolean;
135
- /** The admitting identity's toolkit binding (documentation/toolkits.md). */
136
- toolkitBinding?: ToolkitBinding;
137
113
  }
138
114
  | { ok: false; response: Response }
139
115
  > {
@@ -145,24 +121,6 @@ export async function authorize(
145
121
  const result = await provider.authorize(request, baseUrl);
146
122
  if (result.ok) {
147
123
  const subjectId = result.subjectId ?? result.userId;
148
- // Re-validate both halves and cap the per-identity one by the provider's
149
- // declaration (see resolveIdentityBinding). A binding that does not
150
- // type-check at runtime refuses the request rather than evaporating:
151
- // dropping it would hand the caller the full registry, which is the one
152
- // outcome a binding exists to prevent.
153
- const binding = resolveIdentityBinding(
154
- provider.toolkitBinding,
155
- result.toolkitBinding,
156
- );
157
- if (!binding.ok) {
158
- logger.warn(
159
- `[connecta] refused a request admitted by inbound auth provider ` +
160
- `"${provider.kind}" with 403: ${binding.reason}. Until it is fixed ` +
161
- "this provider cannot admit anyone, because connecta cannot tell " +
162
- "which toolkits the identity may use.",
163
- );
164
- return { ok: false, response: unusableBinding() };
165
- }
166
124
  const actorNamespace = activityActorNamespace(provider);
167
125
  return {
168
126
  ok: true,
@@ -176,7 +134,6 @@ export async function authorize(
176
134
  ...(result.userId && provider.uiAuth?.kind === "clerk"
177
135
  ? { uiAdminEligible: true }
178
136
  : {}),
179
- ...(binding.binding ? { toolkitBinding: binding.binding } : {}),
180
137
  };
181
138
  }
182
139
  lastResponse = result.response;
@@ -195,49 +152,20 @@ export async function authorize(
195
152
  };
196
153
  }
197
154
 
198
- /**
199
- * True when this identity is confined to one or more toolkits — bound, without
200
- * `unscoped`. Such a credential belongs to a team's agent, not to the operator
201
- * running the deployment, so the deployment-wide operator surfaces (`/ui/data`,
202
- * `/ui/activity`, the credential API) refuse it: their payloads describe every
203
- * connector in the org, which is exactly what the binding exists to withhold.
204
- */
205
- export function isToolkitRestricted(
206
- binding: ToolkitBinding | undefined,
207
- ): boolean {
208
- return Boolean(binding && !binding.unscoped);
209
- }
210
-
211
- /** The refusal the deployment-wide operator surfaces give a bound identity. */
212
- export function restrictedOperatorSurface(): Response {
213
- return privateJson(
214
- {
215
- error:
216
- "this credential is bound to a toolkit and may not read " +
217
- "deployment-wide operator data",
218
- },
219
- { status: 403 },
220
- );
221
- }
222
-
223
155
  export async function authorizeUiAdmin(
224
156
  request: Request,
225
157
  baseUrl: string,
226
158
  auth: InboundAuth[],
227
- logger: Logger,
228
159
  purpose = "credential management",
229
160
  ): Promise<{ ok: true; userId: string } | { ok: false; response: Response }> {
230
161
  // Operator mutation is intentionally narrower than /mcp and /ui/data: only
231
162
  // an interactive Clerk provider may admit it. A static bearer token is useful
232
163
  // for headless tool calls but must not become a deployment-admin key.
233
164
  //
234
- // EVERY Clerk provider gets a turn, the way the /mcp gate does, because the
235
- // documented per-team pattern is several `clerkAuth(...)`s that differ only in
236
- // `gate` and `toolkits` (documentation/toolkits.md). Stopping at the first would make
237
- // admission depend on config order: the team-bound provider listed first
238
- // would refuse the operator outright, and a refusal here — a failed gate, a
239
- // missing user, a toolkit-bound identity — is exactly the case where a later
240
- // provider is the one meant to admit. The last refusal is returned if none do.
165
+ // Every Clerk provider gets a turn, the way the /mcp gate does. Stopping at
166
+ // the first would make admission depend on config order: a failed gate or
167
+ // missing user may simply mean a later provider is the one meant to admit.
168
+ // The last refusal is returned if none do.
241
169
  const providers = auth.filter(
242
170
  (candidate) => candidate.uiAuth?.kind === "clerk",
243
171
  );
@@ -264,24 +192,6 @@ export async function authorizeUiAdmin(
264
192
  );
265
193
  continue;
266
194
  }
267
- const binding = resolveIdentityBinding(
268
- provider.toolkitBinding,
269
- result.toolkitBinding,
270
- );
271
- if (!binding.ok) {
272
- logger.warn(
273
- `[connecta] refused an operator-mutation request admitted by inbound auth ` +
274
- `provider "${provider.kind}" with 403: ${binding.reason}.`,
275
- );
276
- lastResponse = unusableBinding();
277
- continue;
278
- }
279
- // A toolkit-bound identity is a team's credential, not a vault admin key:
280
- // credentials are deployment-wide, so writing one reaches every toolkit.
281
- if (isToolkitRestricted(binding.binding)) {
282
- lastResponse = restrictedOperatorSurface();
283
- continue;
284
- }
285
195
  return { ok: true, userId: result.userId };
286
196
  }
287
197
  return {