@zackbart/connecta 0.8.1 → 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 (44) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/SECURITY.md +5 -11
  3. package/dist/auth/downstream-oauth.d.ts +15 -6
  4. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  5. package/dist/auth/downstream-oauth.js +60 -11
  6. package/dist/auth/downstream-oauth.js.map +1 -1
  7. package/dist/connectors/remote-mcp.d.ts +1 -1
  8. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  9. package/dist/connectors/remote-mcp.js +65 -50
  10. package/dist/connectors/remote-mcp.js.map +1 -1
  11. package/dist/errors.d.ts +1 -1
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +1 -0
  14. package/dist/errors.js.map +1 -1
  15. package/dist/execute.d.ts +3 -1
  16. package/dist/execute.d.ts.map +1 -1
  17. package/dist/execute.js +38 -7
  18. package/dist/execute.js.map +1 -1
  19. package/dist/meta-tools.d.ts +1 -1
  20. package/dist/meta-tools.d.ts.map +1 -1
  21. package/dist/meta-tools.js +17 -15
  22. package/dist/meta-tools.js.map +1 -1
  23. package/dist/routes/mcp.d.ts.map +1 -1
  24. package/dist/routes/mcp.js +63 -44
  25. package/dist/routes/mcp.js.map +1 -1
  26. package/dist/routes/oauth.js +1 -1
  27. package/dist/routes/oauth.js.map +1 -1
  28. package/dist/routes/shared.d.ts +2 -2
  29. package/dist/routes/shared.d.ts.map +1 -1
  30. package/dist/types.d.ts +6 -2
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +3 -2
  35. package/src/auth/downstream-oauth.ts +106 -24
  36. package/src/connectors/remote-mcp.ts +96 -64
  37. package/src/errors.ts +2 -0
  38. package/src/execute.ts +40 -6
  39. package/src/meta-tools.ts +20 -16
  40. package/src/routes/mcp.ts +70 -44
  41. package/src/routes/oauth.ts +1 -1
  42. package/src/routes/shared.ts +2 -2
  43. package/src/types.ts +10 -2
  44. package/src/version.ts +1 -1
@@ -1,13 +1,17 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
3
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1
+ import {
2
+ Client,
3
+ isInputRequiredResult,
4
+ specTypeSchemas,
5
+ StreamableHTTPClientTransport,
6
+ UnauthorizedError,
7
+ } from "@modelcontextprotocol/client";
4
8
  import type {
5
9
  FetchLike,
10
+ ListToolsResult,
11
+ StandardSchemaV1,
12
+ Tool,
6
13
  Transport,
7
- } from "@modelcontextprotocol/sdk/shared/transport.js";
8
- import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
9
- import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
10
- import { z } from "zod";
14
+ } from "@modelcontextprotocol/client";
11
15
  import { KvOAuthProvider } from "../auth/downstream-oauth.js";
12
16
  import { MAX_CATALOG_TOOLS } from "../catalog-limits.js";
13
17
  import { ConnectorCallError } from "../errors.js";
@@ -131,36 +135,29 @@ type ListedTool = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
131
135
  * end-of-pagination as `null`. Only the cursor is widened; every tool and every
132
136
  * other result field still passes through the SDK's pinned schema.
133
137
  */
134
- const CompatibleListToolsResultSchema = ListToolsResultSchema.extend({
135
- nextCursor: z.string().nullable().optional(),
136
- });
137
-
138
- /**
139
- * Re-prime an SDK client's tool-metadata cache from the *full* walked catalog.
140
- *
141
- * The SDK's `Client.listTools()` caches one page at a time and **clears** the
142
- * output-schema validators and task-support sets before each replacement.
143
- * This walk uses `Client.request()` so it can make the narrow null-cursor
144
- * compatibility concession above, then primes the metadata exactly once from
145
- * the complete chain. Otherwise `callTool` would find no validator or task
146
- * requirement for earlier-page tools and enforcement would depend on where a
147
- * tool happened to land, which is not enforcement.
148
- *
149
- * So hand the whole aggregated list back deliberately, once, at the end. The
150
- * SDK types the method `private`, hence the cast; the SDK version is pinned
151
- * exactly and `test/remote-mcp-pagination.test.ts` asserts the method still
152
- * exists, so a bump that renames it fails CI rather than quietly restoring the
153
- * bug.
154
- */
155
- function primeToolMetadata(client: Client, tools: ListedTool[]): void {
156
- const prime = (
157
- client as unknown as {
158
- cacheToolMetadata?: (tools: ListedTool[]) => void;
159
- }
160
- ).cacheToolMetadata;
161
- if (typeof prime !== "function") return;
162
- prime.call(client, tools);
163
- }
138
+ const CompatibleListToolsResultSchema: StandardSchemaV1<
139
+ unknown,
140
+ ListToolsResult
141
+ > = {
142
+ "~standard": {
143
+ version: 1,
144
+ vendor: "connecta",
145
+ validate(value) {
146
+ const normalized =
147
+ typeof value === "object" &&
148
+ value !== null &&
149
+ "nextCursor" in value &&
150
+ value.nextCursor === null
151
+ ? (() => {
152
+ const copy = { ...value };
153
+ delete copy.nextCursor;
154
+ return copy;
155
+ })()
156
+ : value;
157
+ return specTypeSchemas.ListToolsResult["~standard"].validate(normalized);
158
+ },
159
+ },
160
+ };
164
161
 
165
162
  /**
166
163
  * True for a result-parse failure caused by the page's `nextCursor` itself.
@@ -171,13 +168,18 @@ function primeToolMetadata(client: Client, tools: ListedTool[]): void {
171
168
  */
172
169
  function isCursorShapeError(err: unknown): boolean {
173
170
  const issues = (err as { issues?: unknown } | null)?.issues;
174
- return (
171
+ if (
175
172
  Array.isArray(issues) &&
176
173
  issues.some((issue) => {
177
174
  const path = (issue as { path?: unknown }).path;
178
175
  return Array.isArray(path) && path[0] === "nextCursor";
179
176
  })
180
- );
177
+ ) {
178
+ return true;
179
+ }
180
+ // SDK v2 wraps Standard Schema failures in a ProtocolError and preserves the
181
+ // failing path in the message rather than exposing the validator's issues.
182
+ return msg(err).startsWith("Invalid result for tools/list: nextCursor:");
181
183
  }
182
184
 
183
185
  function msg(err: unknown): string {
@@ -393,6 +395,14 @@ export function redirectSafeFetch(
393
395
  interface ConnectionState {
394
396
  client: Client | null;
395
397
  transport: Transport | null;
398
+ /**
399
+ * The last complete raw catalog, retained only for this request scope.
400
+ *
401
+ * SDK v2 exposes `toolDefinition` as the public call-time seam for output
402
+ * validation and header mirroring, replacing the v1 private
403
+ * `cacheToolMetadata` reach-through.
404
+ */
405
+ toolDefinitions: Map<string, Tool>;
396
406
  connecting: Promise<void> | null;
397
407
  authRequired: boolean;
398
408
  provider: KvOAuthProvider | null;
@@ -511,6 +521,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
511
521
  state = {
512
522
  client: null,
513
523
  transport: null,
524
+ toolDefinitions: new Map(),
514
525
  connecting: null,
515
526
  authRequired: false,
516
527
  provider: null,
@@ -549,29 +560,23 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
549
560
  const url = new URL(opts.url);
550
561
  const guardedFetch = redirectSafeFetch(id, opts.redirects);
551
562
  if (opts.auth?.type === "oauth") {
552
- // The SDK class declares `sessionId` as an own `string | undefined`
553
- // property while its Transport interface declares it optional. They are
554
- // runtime-compatible; exact optional types only exposes that declaration
555
- // mismatch at this boundary.
556
563
  return new StreamableHTTPClientTransport(url, {
557
564
  authProvider: provider ?? newProvider(ctx),
558
565
  fetch: guardedFetch,
559
- }) as unknown as Transport;
566
+ });
560
567
  }
561
568
  const headers =
562
569
  opts.auth?.type === "headers" ? opts.auth.headers : undefined;
563
- return new StreamableHTTPClientTransport(
564
- url,
565
- {
566
- ...(headers ? { requestInit: { headers } } : {}),
567
- fetch: guardedFetch,
568
- },
569
- ) as unknown as Transport;
570
+ return new StreamableHTTPClientTransport(url, {
571
+ ...(headers ? { requestInit: { headers } } : {}),
572
+ fetch: guardedFetch,
573
+ });
570
574
  };
571
575
 
572
576
  const reset = (state: ConnectionState) => {
573
577
  state.client = null;
574
578
  state.transport = null;
579
+ state.toolDefinitions.clear();
575
580
  state.connecting = null;
576
581
  state.authRequired = false;
577
582
  state.provider = null;
@@ -652,13 +657,17 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
652
657
  throw operatorDisconnectedError();
653
658
  }
654
659
  provider?.captureGeneration(genAtStart);
655
- // The SDK defaults to AJV, which compiles every advertised outputSchema
656
- // with `new Function`. Cloudflare Workers prohibit dynamic code
657
- // generation, so a remote such as Stripe fails during tools/list unless
658
- // the SDK's edge-safe validator is selected explicitly.
660
+ // SDK v2 selects its validator by runtime export condition: AJV on
661
+ // Node and @cfworker/json-schema under workerd. The Workers-safe path
662
+ // no longer needs Connecta-specific wiring.
659
663
  const c = new Client(
660
664
  { name: "connecta", version: CONNECTA_VERSION },
661
- { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() },
665
+ {
666
+ versionNegotiation: { mode: "auto" },
667
+ // Connecta has no interactive relay. Surface the result manually
668
+ // below as one structured, non-retryable connector failure.
669
+ inputRequired: { autoFulfill: false },
670
+ },
662
671
  );
663
672
  const t = buildTransport(ctx, provider);
664
673
  if (!ownsAttempt()) await abandon(t);
@@ -871,9 +880,9 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
871
880
  `Connector "${id}" kept advertising more tools/list pages after ${MAX_TOOL_PAGES} — refusing to page further.`,
872
881
  );
873
882
  }
874
- // Repair what the per-page listTools calls left behind before any of
875
- // these tools can be called. See primeToolMetadata.
876
- primeToolMetadata(client, listed);
883
+ // Publish definitions only after the full walk succeeds. A later-page
884
+ // failure must not leave a partial validation/header view behind.
885
+ state.toolDefinitions = new Map(listed.map((tool) => [tool.name, tool]));
877
886
  return listed.map((t) => ({
878
887
  name: t.name,
879
888
  ...(t.description !== undefined ? { description: t.description } : {}),
@@ -906,14 +915,32 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
906
915
  await ensureConnected(ctx, state);
907
916
  const client = state.client!;
908
917
  try {
909
- return await client.callTool(
918
+ const toolDefinition = state.toolDefinitions.get(name);
919
+ if (toolDefinition?.execution?.taskSupport === "required") {
920
+ throw new Error(
921
+ `Tool "${name}" requires task-based execution, which Connecta does not support.`,
922
+ );
923
+ }
924
+ const result = await client.callTool(
910
925
  {
911
926
  name,
912
927
  arguments: (args ?? {}) as Record<string, unknown>,
913
928
  },
914
- undefined,
915
- requestOptions(ctx),
929
+ {
930
+ ...requestOptions(ctx),
931
+ allowInputRequired: true,
932
+ ...(toolDefinition ? { toolDefinition } : {}),
933
+ },
916
934
  );
935
+ if (isInputRequiredResult(result)) {
936
+ throw new ConnectorCallError(
937
+ "input_required_unsupported",
938
+ `Connector "${id}" returned input_required for "${name}". ` +
939
+ "Connecta cannot relay multi-round-trip input yet; this " +
940
+ "capability is gated pending real host and downstream adoption.",
941
+ );
942
+ }
943
+ return result;
917
944
  } catch (err) {
918
945
  // A grant revoked after connect surfaces here, not in ensureConnected.
919
946
  if (err instanceof UnauthorizedError) {
@@ -939,6 +966,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
939
966
  const transport = state.transport;
940
967
  state.client = null;
941
968
  state.transport = null;
969
+ state.toolDefinitions.clear();
942
970
  state.connecting = null;
943
971
  state.authRequired = false;
944
972
  state.connectedGeneration = null;
@@ -979,7 +1007,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
979
1007
  }
980
1008
  },
981
1009
 
982
- async finishAuth(code, ctx) {
1010
+ async finishAuth(code, ctx, callbackParams) {
983
1011
  const state = stateFor(ctx);
984
1012
  const provider = getProvider(ctx, state);
985
1013
  // verifyState ran on this request-scoped provider first and captured the
@@ -987,7 +1015,11 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
987
1015
  // token write remains tagged with that older generation and is unreadable.
988
1016
  const t = (state.transport ??
989
1017
  buildTransport(ctx, provider)) as StreamableHTTPClientTransport;
990
- await t.finishAuth(code);
1018
+ if (callbackParams !== undefined) {
1019
+ await t.finishAuth(callbackParams);
1020
+ } else {
1021
+ await t.finishAuth(code);
1022
+ }
991
1023
  await provider.clearPending();
992
1024
  // Reset so the next use reconnects with the freshly stored tokens.
993
1025
  reset(state);
package/src/errors.ts CHANGED
@@ -8,6 +8,7 @@ export type ConnectorCallErrorCode =
8
8
  | "rate_limited"
9
9
  | "unavailable"
10
10
  | "invalid_args"
11
+ | "input_required_unsupported"
11
12
  | "connector_call_failed";
12
13
 
13
14
  /** Agent-visible recovery class attached only to `auth_required` failures. */
@@ -22,6 +23,7 @@ const RETRYABLE_BY_CODE: Record<ConnectorCallErrorCode, boolean> = {
22
23
  unavailable: true,
23
24
  auth_required: false,
24
25
  invalid_args: false,
26
+ input_required_unsupported: false,
25
27
  connector_call_failed: false,
26
28
  };
27
29
 
package/src/execute.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
2
  import { z } from "zod";
3
3
  import type { ActivityRequestContext } from "./activity.js";
4
4
  import {
@@ -150,6 +150,7 @@ export async function buildSandboxProviders(
150
150
  maxHostCalls?: number;
151
151
  hostCallTimeoutMs?: number;
152
152
  discoveryConcurrency?: number;
153
+ onInvocationFailure?: (failure: InvocationFailure) => void;
153
154
  } = {},
154
155
  ): Promise<ExecutorProvider[]> {
155
156
  // All host calls made by one execute_code invocation share a downstream
@@ -210,7 +211,11 @@ export async function buildSandboxProviders(
210
211
  args ?? {},
211
212
  invocationContext(),
212
213
  );
213
- if (!outcome.ok) throw new InvocationFailure(outcome.error);
214
+ if (!outcome.ok) {
215
+ const failure = new InvocationFailure(outcome.error);
216
+ limits.onInvocationFailure?.(failure);
217
+ throw failure;
218
+ }
214
219
  return outcome.value;
215
220
  };
216
221
  const callNamespace = async (
@@ -225,7 +230,11 @@ export async function buildSandboxProviders(
225
230
  args ?? {},
226
231
  invocationContext(),
227
232
  );
228
- if (!outcome.ok) throw new InvocationFailure(outcome.error);
233
+ if (!outcome.ok) {
234
+ const failure = new InvocationFailure(outcome.error);
235
+ limits.onInvocationFailure?.(failure);
236
+ throw failure;
237
+ }
229
238
  return outcome.value;
230
239
  };
231
240
 
@@ -317,6 +326,7 @@ export function createExecuteTool(
317
326
  }
318
327
  let lease;
319
328
  let outcome;
329
+ const invocationFailures: InvocationFailure[] = [];
320
330
  try {
321
331
  // Admission comes before provider construction: queued calls retain no
322
332
  // catalogs, request scopes, or one-closure-per-tool provider arrays.
@@ -335,6 +345,9 @@ export function createExecuteTool(
335
345
  activity,
336
346
  {
337
347
  signal: controller.signal,
348
+ onInvocationFailure: (failure) => {
349
+ invocationFailures.push(failure);
350
+ },
338
351
  ...(config.discoveryConcurrency !== undefined
339
352
  ? { discoveryConcurrency: config.discoveryConcurrency }
340
353
  : {}),
@@ -386,6 +399,27 @@ export function createExecuteTool(
386
399
  )
387
400
  : undefined;
388
401
  if (outcome.error) {
402
+ // Executor bridges necessarily reduce thrown host errors to strings.
403
+ // Match that terminal string back to the request-local typed failure so
404
+ // an unhandled tool failure keeps the same structured contract as
405
+ // call_tool and batch_call. Failures caught by model code never reach
406
+ // outcome.error and therefore remain under that code's control.
407
+ let invocationFailure: InvocationFailure | undefined;
408
+ for (let i = invocationFailures.length - 1; i >= 0; i--) {
409
+ const candidate = invocationFailures[i];
410
+ if (candidate && outcome.error.includes(candidate.message)) {
411
+ invocationFailure = candidate;
412
+ break;
413
+ }
414
+ }
415
+ if (invocationFailure) {
416
+ const result = jsonResult({
417
+ error: invocationFailure.details,
418
+ ...(logs ? { logs } : {}),
419
+ });
420
+ result.isError = true;
421
+ return result;
422
+ }
389
423
  return errorResult(
390
424
  `Error: ${outcome.error}${logs ? `\n\nLogs:\n${logs}` : ""}`,
391
425
  );
@@ -448,11 +482,11 @@ export function registerExecuteTool(
448
482
  "execute_code",
449
483
  {
450
484
  description: EXECUTE_DESC,
451
- inputSchema: {
485
+ inputSchema: z.object({
452
486
  code: z
453
487
  .string()
454
488
  .describe("A JavaScript async arrow function to execute."),
455
- },
489
+ }),
456
490
  // The sandbox exposes only tools that are explicitly read-only, and the
457
491
  // executor grants no network, filesystem, env, or timer capabilities.
458
492
  annotations: {
@@ -463,7 +497,7 @@ export function registerExecuteTool(
463
497
  },
464
498
  async (args, extra) => {
465
499
  const controller = new AbortController();
466
- const signals = [extra.signal, ctx.requestSignal].filter(
500
+ const signals = [extra.mcpReq.signal, ctx.requestSignal].filter(
467
501
  (signal): signal is AbortSignal => signal !== undefined,
468
502
  );
469
503
  const forwarders = signals.map((signal) => {
package/src/meta-tools.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
2
  import { z } from "zod";
3
3
  import type {
4
4
  ActivityCallSource,
@@ -611,6 +611,7 @@ export function createMetaTools(
611
611
  if (!outcome.ok) {
612
612
  const failedResult =
613
613
  outcome.error.code === "auth_required" ||
614
+ outcome.error.code === "input_required_unsupported" ||
614
615
  call.resultMode === "value"
615
616
  ? jsonResult({
616
617
  ok: false,
@@ -620,7 +621,10 @@ export function createMetaTools(
620
621
  ...(call.diagnostics ? { timing: outcome.timing } : {}),
621
622
  })
622
623
  : errorResult(outcome.error.message);
623
- if (outcome.error.code === "auth_required") {
624
+ if (
625
+ outcome.error.code === "auth_required" ||
626
+ outcome.error.code === "input_required_unsupported"
627
+ ) {
624
628
  failedResult.isError = true;
625
629
  }
626
630
  return {
@@ -1222,7 +1226,7 @@ export function registerMetaTools(
1222
1226
  "skills",
1223
1227
  {
1224
1228
  description: describedFor(registry, SKILLS_DESC, "skills"),
1225
- inputSchema: { name: z.string().optional() },
1229
+ inputSchema: z.object({ name: z.string().optional() }),
1226
1230
  annotations: READ_ONLY_LOCAL,
1227
1231
  },
1228
1232
  async (args) => mt.skills(args as SkillArgs),
@@ -1232,7 +1236,7 @@ export function registerMetaTools(
1232
1236
  "list_connectors",
1233
1237
  {
1234
1238
  description: LIST_DESC,
1235
- inputSchema: { probe: z.boolean().optional() },
1239
+ inputSchema: z.object({ probe: z.boolean().optional() }),
1236
1240
  annotations: READ_ONLY_REMOTE,
1237
1241
  },
1238
1242
  async (args) => mt.listConnectors(args as ListArgs),
@@ -1242,14 +1246,14 @@ export function registerMetaTools(
1242
1246
  "search_tools",
1243
1247
  {
1244
1248
  description: describedFor(registry, SEARCH_DESC, "search"),
1245
- inputSchema: {
1249
+ inputSchema: z.object({
1246
1250
  query: z.string().optional(),
1247
1251
  connector: z.string().optional(),
1248
1252
  limit: z.number().int().positive().max(MAX_SEARCH_LIMIT).optional(),
1249
1253
  offset: z.number().int().nonnegative().optional(),
1250
1254
  fullDescriptions: z.boolean().optional(),
1251
1255
  includeSchemas: z.enum(["compact", "json"]).optional(),
1252
- },
1256
+ }),
1253
1257
  annotations: READ_ONLY_REMOTE,
1254
1258
  },
1255
1259
  async (args) => mt.searchTools(args as SearchArgs),
@@ -1259,11 +1263,11 @@ export function registerMetaTools(
1259
1263
  "describe_tools",
1260
1264
  {
1261
1265
  description: describedFor(registry, DESCRIBE_DESC, "describe"),
1262
- inputSchema: {
1266
+ inputSchema: z.object({
1263
1267
  addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
1264
1268
  format: z.enum(["compact", "json"]).optional(),
1265
1269
  fullDescriptions: z.boolean().optional(),
1266
- },
1270
+ }),
1267
1271
  annotations: READ_ONLY_REMOTE,
1268
1272
  },
1269
1273
  async (args) => mt.describeTools(args as DescribeArgs),
@@ -1273,7 +1277,7 @@ export function registerMetaTools(
1273
1277
  "call_tool",
1274
1278
  {
1275
1279
  description: CALL_DESC,
1276
- inputSchema: CALL_INPUT_SCHEMA,
1280
+ inputSchema: z.object(CALL_INPUT_SCHEMA),
1277
1281
  // call_tool admits only tools that are themselves explicitly read-only;
1278
1282
  // anything else is refused and routed to call_destructive_tool.
1279
1283
  annotations: READ_ONLY_REMOTE,
@@ -1285,7 +1289,7 @@ export function registerMetaTools(
1285
1289
  "call_destructive_tool",
1286
1290
  {
1287
1291
  description: CALL_DESTRUCTIVE_DESC,
1288
- inputSchema: CALL_INPUT_SCHEMA,
1292
+ inputSchema: z.object(CALL_INPUT_SCHEMA),
1289
1293
  annotations: {
1290
1294
  destructiveHint: true,
1291
1295
  readOnlyHint: false,
@@ -1299,10 +1303,10 @@ export function registerMetaTools(
1299
1303
  "authorize_connector",
1300
1304
  {
1301
1305
  description: AUTHORIZE_DESC,
1302
- inputSchema: {
1306
+ inputSchema: z.object({
1303
1307
  connector: z.string(),
1304
1308
  force: z.boolean().optional(),
1305
- },
1309
+ }),
1306
1310
  // Starts (or with force, resets) a downstream OAuth flow — it changes
1307
1311
  // stored connector auth state, so it is deliberately not read-only.
1308
1312
  annotations: {
@@ -1318,7 +1322,7 @@ export function registerMetaTools(
1318
1322
  "get_result",
1319
1323
  {
1320
1324
  description: GET_RESULT_DESC,
1321
- inputSchema: {
1325
+ inputSchema: z.object({
1322
1326
  id: z.string(),
1323
1327
  // Both bounds are the shared rules (isValidResultOffset,
1324
1328
  // isValidMaxResultBytes) expressed for the wire: spelling them against
@@ -1326,7 +1330,7 @@ export function registerMetaTools(
1326
1330
  // in-handler checks if either floor ever moves.
1327
1331
  offset: z.number().int().min(MIN_RESULT_OFFSET).optional(),
1328
1332
  maxBytes: z.number().int().min(MIN_MAX_RESULT_BYTES).optional(),
1329
- },
1333
+ }),
1330
1334
  annotations: READ_ONLY_LOCAL,
1331
1335
  },
1332
1336
  async (args) => mt.getResult(args as GetResultArgs),
@@ -1336,7 +1340,7 @@ export function registerMetaTools(
1336
1340
  "batch_call",
1337
1341
  {
1338
1342
  description: BATCH_DESC,
1339
- inputSchema: {
1343
+ inputSchema: z.object({
1340
1344
  calls: z
1341
1345
  .array(z.object(CALL_INPUT_SCHEMA))
1342
1346
  .min(1)
@@ -1345,7 +1349,7 @@ export function registerMetaTools(
1345
1349
  timeoutMs: z.number().int().positive().optional(),
1346
1350
  maxRetries: z.number().int().min(0).max(2).optional(),
1347
1351
  diagnostics: z.boolean().optional(),
1348
- },
1352
+ }),
1349
1353
  // Same gate as call_tool: every call in the batch must be explicitly
1350
1354
  // read-only or the batch is refused.
1351
1355
  annotations: READ_ONLY_REMOTE,
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 {
@@ -21,7 +25,7 @@ export const MCP_CORS_HEADERS = {
21
25
  "Access-Control-Allow-Origin": "*",
22
26
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
23
27
  "Access-Control-Allow-Headers":
24
- "Content-Type, Authorization, mcp-protocol-version, mcp-session-id",
28
+ "Content-Type, Authorization, mcp-protocol-version, mcp-session-id, mcp-method, mcp-name",
25
29
  };
26
30
 
27
31
  // Browser-based MCP clients call /mcp cross-origin. Without CORS on every
@@ -188,54 +192,76 @@ async function serveMcp(
188
192
  registry: RegistryView,
189
193
  runtimeContext?: RuntimeExecutionContext,
190
194
  ): Promise<Response> {
191
- // Fresh McpServer + transport per request (SDK ≥1.26 requirement), stateless.
192
- const server = new McpServer(opts.serverInfo, {
193
- instructions: CONNECTA_INSTRUCTIONS,
194
- });
195
- const activity: ActivityRequestContext | undefined = opts.activity
196
- ? {
197
- sink: opts.activity,
198
- actor,
199
- requestId: crypto.randomUUID(),
200
- serverInfo: opts.serverInfo,
201
- ...(opts.activityDeploymentId
202
- ? { deploymentId: opts.activityDeploymentId }
203
- : {}),
204
- ...(runtimeContext?.waitUntil
205
- ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
206
- : {}),
207
- logger: opts.logger,
208
- }
209
- : undefined;
210
- registerMetaTools(server, registry, {
211
- baseUrl,
212
- ...(activity ? { activity } : {}),
213
- ...(opts.defaultToolTimeoutMs !== undefined
214
- ? { defaultToolTimeoutMs: opts.defaultToolTimeoutMs }
215
- : {}),
216
- ...(opts.probeTimeoutMs !== undefined
217
- ? { probeTimeoutMs: opts.probeTimeoutMs }
218
- : {}),
219
- ...(opts.discoveryConcurrency !== undefined
220
- ? { discoveryConcurrency: opts.discoveryConcurrency }
221
- : {}),
222
- requestSignal: request.signal,
223
- ...(runtimeContext
224
- ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
225
- : {}),
226
- });
227
- if (opts.executor) {
228
- 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, {
229
221
  baseUrl,
230
- executor: opts.executor,
231
- logger: opts.logger,
232
222
  ...(activity ? { activity } : {}),
233
- requestSignal: request.signal,
223
+ ...(opts.defaultToolTimeoutMs !== undefined
224
+ ? { defaultToolTimeoutMs: opts.defaultToolTimeoutMs }
225
+ : {}),
226
+ ...(opts.probeTimeoutMs !== undefined
227
+ ? { probeTimeoutMs: opts.probeTimeoutMs }
228
+ : {}),
234
229
  ...(opts.discoveryConcurrency !== undefined
235
230
  ? { discoveryConcurrency: opts.discoveryConcurrency }
236
231
  : {}),
232
+ requestSignal: request.signal,
233
+ ...(runtimeContext
234
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
235
+ : {}),
237
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);
238
261
  }
262
+
263
+ // Fresh server + transport per legacy request, stateless and JSON-shaped.
264
+ const server = createServer();
239
265
  const transport = new WebStandardStreamableHTTPServerTransport({
240
266
  enableJsonResponse: true,
241
267
  });
@@ -309,7 +309,7 @@ export async function routeOAuthCallback(
309
309
  return refused();
310
310
  }
311
311
  try {
312
- await connector.finishAuth(code, connectorContext);
312
+ await connector.finishAuth(code, connectorContext, url.searchParams);
313
313
  await opts.registry.invalidateStored(id);
314
314
  return html(
315
315
  `Connected "${id}". You can close this window.`,