@zackbart/connecta 0.8.1 → 0.9.1

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 (60) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +9 -8
  3. package/SECURITY.md +5 -11
  4. package/dist/auth/downstream-oauth.d.ts +15 -6
  5. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  6. package/dist/auth/downstream-oauth.js +60 -11
  7. package/dist/auth/downstream-oauth.js.map +1 -1
  8. package/dist/catalog-service.d.ts +8 -0
  9. package/dist/catalog-service.d.ts.map +1 -1
  10. package/dist/catalog-service.js +24 -2
  11. package/dist/catalog-service.js.map +1 -1
  12. package/dist/catalog.d.ts +34 -1
  13. package/dist/catalog.d.ts.map +1 -1
  14. package/dist/catalog.js +264 -40
  15. package/dist/catalog.js.map +1 -1
  16. package/dist/connectors/remote-mcp.d.ts +1 -1
  17. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  18. package/dist/connectors/remote-mcp.js +65 -50
  19. package/dist/connectors/remote-mcp.js.map +1 -1
  20. package/dist/errors.d.ts +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +1 -0
  23. package/dist/errors.js.map +1 -1
  24. package/dist/execute.d.ts +3 -1
  25. package/dist/execute.d.ts.map +1 -1
  26. package/dist/execute.js +50 -13
  27. package/dist/execute.js.map +1 -1
  28. package/dist/meta-tools.d.ts +1 -1
  29. package/dist/meta-tools.d.ts.map +1 -1
  30. package/dist/meta-tools.js +19 -17
  31. package/dist/meta-tools.js.map +1 -1
  32. package/dist/routes/mcp.d.ts.map +1 -1
  33. package/dist/routes/mcp.js +63 -44
  34. package/dist/routes/mcp.js.map +1 -1
  35. package/dist/routes/oauth.js +1 -1
  36. package/dist/routes/oauth.js.map +1 -1
  37. package/dist/routes/shared.d.ts +2 -2
  38. package/dist/routes/shared.d.ts.map +1 -1
  39. package/dist/skills.d.ts +2 -2
  40. package/dist/skills.d.ts.map +1 -1
  41. package/dist/skills.js +7 -7
  42. package/dist/skills.js.map +1 -1
  43. package/dist/types.d.ts +6 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +3 -2
  48. package/src/auth/downstream-oauth.ts +106 -24
  49. package/src/catalog-service.ts +43 -0
  50. package/src/catalog.ts +327 -33
  51. package/src/connectors/remote-mcp.ts +96 -64
  52. package/src/errors.ts +2 -0
  53. package/src/execute.ts +55 -12
  54. package/src/meta-tools.ts +22 -18
  55. package/src/routes/mcp.ts +70 -44
  56. package/src/routes/oauth.ts +1 -1
  57. package/src/routes/shared.ts +2 -2
  58. package/src/skills.ts +7 -7
  59. package/src/types.ts +10 -2
  60. package/src/version.ts +1 -1
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
 
@@ -270,11 +279,20 @@ export async function buildSandboxProviders(
270
279
  offset?: number;
271
280
  fullDescriptions?: boolean;
272
281
  includeSchemas?: "compact" | "json";
282
+ includeSchemaKeys?: boolean;
273
283
  };
274
- const result = flatSearchResult(await catalog.search(args));
284
+ const result = flatSearchResult(
285
+ await catalog.search({
286
+ ...args,
287
+ // Key metadata rides along with schemas by default, since that is
288
+ // the whole point of it in code mode. It stays opt-out because it
289
+ // counts against the same hard discovery-byte ceiling.
290
+ includeSchemaKeys: args.includeSchemaKeys !== false,
291
+ }),
292
+ );
275
293
  boundedDiscoveryText(
276
294
  result,
277
- "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
295
+ "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.",
278
296
  );
279
297
  return result;
280
298
  },
@@ -317,6 +335,7 @@ export function createExecuteTool(
317
335
  }
318
336
  let lease;
319
337
  let outcome;
338
+ const invocationFailures: InvocationFailure[] = [];
320
339
  try {
321
340
  // Admission comes before provider construction: queued calls retain no
322
341
  // catalogs, request scopes, or one-closure-per-tool provider arrays.
@@ -335,6 +354,9 @@ export function createExecuteTool(
335
354
  activity,
336
355
  {
337
356
  signal: controller.signal,
357
+ onInvocationFailure: (failure) => {
358
+ invocationFailures.push(failure);
359
+ },
338
360
  ...(config.discoveryConcurrency !== undefined
339
361
  ? { discoveryConcurrency: config.discoveryConcurrency }
340
362
  : {}),
@@ -386,6 +408,27 @@ export function createExecuteTool(
386
408
  )
387
409
  : undefined;
388
410
  if (outcome.error) {
411
+ // Executor bridges necessarily reduce thrown host errors to strings.
412
+ // Match that terminal string back to the request-local typed failure so
413
+ // an unhandled tool failure keeps the same structured contract as
414
+ // call_tool and batch_call. Failures caught by model code never reach
415
+ // outcome.error and therefore remain under that code's control.
416
+ let invocationFailure: InvocationFailure | undefined;
417
+ for (let i = invocationFailures.length - 1; i >= 0; i--) {
418
+ const candidate = invocationFailures[i];
419
+ if (candidate && outcome.error.includes(candidate.message)) {
420
+ invocationFailure = candidate;
421
+ break;
422
+ }
423
+ }
424
+ if (invocationFailure) {
425
+ const result = jsonResult({
426
+ error: invocationFailure.details,
427
+ ...(logs ? { logs } : {}),
428
+ });
429
+ result.isError = true;
430
+ return result;
431
+ }
389
432
  return errorResult(
390
433
  `Error: ${outcome.error}${logs ? `\n\nLogs:\n${logs}` : ""}`,
391
434
  );
@@ -408,18 +451,18 @@ export function createExecuteTool(
408
451
  };
409
452
  }
410
453
 
411
- const EXECUTE_DESC = `Use for dependent multi-step calls, loops, joins, branching, or reducing large results in a sandbox. Only tools explicitly annotated readOnlyHint: true are available. For one straightforward call use call_tool; for 2–10 independent calls use batch_call. Each run is limited to ${EXECUTE_MAX_HOST_CALLS} host calls; connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS}; each host call has a ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second deadline.
454
+ const EXECUTE_DESC = `Use for dependent multi-step calls, loops, joins, branching, or reducing large results in a sandbox. Never use execute_code for search-only discovery or one downstream call: use search_tools, then call_tool when needed. For 2–10 independent calls use batch_call. Only tools explicitly annotated readOnlyHint: true are available. Each run is limited to ${EXECUTE_MAX_HOST_CALLS} host calls; connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS}; each host call has a ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second deadline.
412
455
 
413
456
  Write an async arrow function. It runs with NO network, filesystem, timers, or imports — the only capabilities are:
414
457
  - One global per connector: every address <connectorId>.<toolName> from search_tools is callable as <connectorId>.<toolName>(args) with a single args object matching the schema from describe_tools. Names are sanitized to JS identifiers: characters outside [A-Za-z0-9_$] become "_" (e.g. my-service.get.thing → my_service.get_thing), leading digits get "_" prefixed, reserved words get "_" appended.
415
458
  - connecta.call(address, args) and connecta.batch(calls) — call raw addresses.
416
- - connecta.search(args) and connecta.describe(args) — load and inspect request-local catalogs on demand.
459
+ - connecta.search(args) and connecta.describe(args) — load and inspect request-local catalogs on demand. Matches carrying schemas also list inputKeys, requiredInputKeys, and outputKeys — the same names the schema shows, ready to check against before building args. They are absent when a schema is not a plain object shape, so read the schema itself rather than assuming a missing list means no fields.
417
460
  - console.log(...) — captured and returned alongside the result.
418
461
 
419
462
  Tool calls return plain values (MCP text content is JSON-parsed when possible) and throw on downstream errors — use try/catch to handle them. Return a JSON-serializable value; large results are truncated, so reduce data in code instead of returning raw payloads.
420
463
 
421
- Workflow: search_tools describe_tools (schemas) execute_code. Plain JavaScript only no TypeScript syntax.
422
- Example: async () => { const r = await crm.search({ query: "roadmap" }); return r.results.map((item) => item.title); }`;
464
+ Plain JavaScript only — no TypeScript syntax. For unknown-address dependent work, use one execute_code call: search inside it, read the compact schemas, and continue to the dependent calls; do not return search results for a second execute_code call. Compact schemas are TypeScript-like strings, not JSON Schema objects: write the property names they display, never a positional guess or an invented alias.
465
+ Dependent example (only when the second call requires a value returned by the first): async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", includeSchemas: "compact" }); const pick = (suffix) => { const match = tools.find((tool) => tool.address.endsWith(suffix)); if (!match) throw new Error("no tool matching " + suffix); return match.address; }; const run = await connecta.call(pick(".get_run"), { runId: 42 }); const logs = await connecta.call(pick(".get_job_logs"), { jobId: run.failedJobId }); return [run, logs]; }`;
423
466
 
424
467
  /** Register the execute_code meta-tool. Only called when an executor is configured. */
425
468
  export function registerExecuteTool(
@@ -448,11 +491,11 @@ export function registerExecuteTool(
448
491
  "execute_code",
449
492
  {
450
493
  description: EXECUTE_DESC,
451
- inputSchema: {
494
+ inputSchema: z.object({
452
495
  code: z
453
496
  .string()
454
497
  .describe("A JavaScript async arrow function to execute."),
455
- },
498
+ }),
456
499
  // The sandbox exposes only tools that are explicitly read-only, and the
457
500
  // executor grants no network, filesystem, env, or timer capabilities.
458
501
  annotations: {
@@ -463,7 +506,7 @@ export function registerExecuteTool(
463
506
  },
464
507
  async (args, extra) => {
465
508
  const controller = new AbortController();
466
- const signals = [extra.signal, ctx.requestSignal].filter(
509
+ const signals = [extra.mcpReq.signal, ctx.requestSignal].filter(
467
510
  (signal): signal is AbortSignal => signal !== undefined,
468
511
  );
469
512
  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 {
@@ -1114,8 +1118,8 @@ export function createMetaTools(
1114
1118
 
1115
1119
  const LIST_DESC =
1116
1120
  "List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
1117
- const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. The default page has ${DEFAULT_SEARCH_LIMIT} tools; explicit limit can request up to ${MAX_SEARCH_LIMIT}. includeSchemas="compact" usually removes the describe_tools round trip.`;
1118
- const DESCRIBE_DESC = `Inspect up to ${MAX_DESCRIBE_ADDRESSES} known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.`;
1121
+ const SEARCH_DESC = `Unknown address: use 2–4 distinctive action/object terms, not the full request; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}) and page only if needed, up to ${MAX_SEARCH_LIMIT}. includeSchemas="compact" adds the input and any declared output shape; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
1122
+ const DESCRIBE_DESC = `Only when search_tools omitted schemas, a compact shape is ambiguous, or exact JSON constraints are needed. Inspects up to ${MAX_DESCRIBE_ADDRESSES} addresses with schemas and annotations; "compact" is default, while "json" preserves exact constraints.`;
1119
1123
  const CALL_DESC =
1120
1124
  'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths, resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
1121
1125
  const CALL_DESTRUCTIVE_DESC =
@@ -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.`,
@@ -1,4 +1,4 @@
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";
@@ -18,7 +18,7 @@ export interface ServerOptions {
18
18
  publicUrl?: string;
19
19
  // The SDK's Implementation shape: name/version plus optional title,
20
20
  // websiteUrl, and icons (MCP icons spec) that clients may render.
21
- serverInfo: ConstructorParameters<typeof McpServer>[0];
21
+ serverInfo: Implementation;
22
22
  logger: Logger;
23
23
  activity?: ActivityStore;
24
24
  activityReadGate?: ActivityReadGate;
package/src/skills.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import type { Connector } from "./types.js";
2
2
 
3
3
  export const CONNECTA_INSTRUCTIONS =
4
- 'Connecta exposes many integrations behind meta-tools. When an address is unknown, start with search_tools and includeSchemas="compact"; use describe_tools only when that schema is insufficient. Use call_tool for one explicitly read-only call, batch_call for 2–10 independent explicitly read-only calls, and execute_code (when available) only for dependent read-only steps, loops, joins, or reducing large results. Unannotated, write-capable, and destructive tools must use call_destructive_tool individually. Use authorize_connector only after auth_required and get_result only for truncated results. Fetch skills({ name: "usage" }) when this routing workflow is unfamiliar.';
4
+ 'Connecta exposes integrations behind meta-tools. Unknown address: use search_tools with 2–4 distinctive action/object terms, no initial limit, and includeSchemas="compact"; describe_tools only if that shape is ambiguous or exact JSON constraints are needed. Use call_tool for one explicitly read-only call, batch_call for 2–10 independent read-only calls, and execute_code (when available) only for dependencies, loops, joins, or substantial reduction — searching inside that one run rather than searching first. Use call_destructive_tool individually for unannotated, write-capable, or destructive tools. authorize_connector follows auth_required; get_result follows truncation. If this routing is unfamiliar, fetch skills({ name: "usage" }).';
5
5
 
6
6
  export const USAGE_SKILL = `# Connecta usage
7
7
 
8
8
  ## Choose the smallest execution tool
9
9
 
10
- Use exact addresses returned by discovery; never invent one.
10
+ Use exact addresses returned by discovery; never invent one. Search with 2–4 distinctive action/object terms rather than the full request, and omit \`limit\` initially so the default page stays small.
11
11
 
12
- - Unknown address: \`search_tools({ query, includeSchemas: "compact" })\`.
13
- - Schema still unclear: \`describe_tools({ addresses: [...] })\`.
12
+ - Unknown address: \`search_tools({ query, includeSchemas: "compact" })\`; every match then includes its input shape plus any declared output shape and annotations.
13
+ - Compact shape still ambiguous: \`describe_tools({ addresses: [...] })\`; use \`format: "json"\` only for exact constraints.
14
14
  - One explicitly read-only call: \`call_tool\`.
15
15
  - Two to ten independent explicitly read-only calls: \`batch_call\`.
16
16
  - Dependent read-only calls, loops, joins, branching, or large-result reduction: \`execute_code\` when available.
@@ -18,15 +18,15 @@ Use exact addresses returned by discovery; never invent one.
18
18
  - Truncated result: retry with \`fields\` when possible; otherwise page it with \`get_result\`.
19
19
  - \`auth_required\`: use \`authorize_connector\`, give its recovery handoff to the operator, then retry the original call.
20
20
 
21
- Use \`list_connectors({ probe: false })\` for a fast inventory based on recent call observations and local credential-shape drift. Use \`probe: true\` only when diagnosing live health or authorization.
21
+ Use \`list_connectors({ probe: false })\` for a fast observed-health inventory; use \`probe: true\` only to diagnose live health or authorization.
22
22
 
23
23
  ## Code mode
24
24
 
25
- Use code mode when calls depend on earlier results, when joining connectors, or when sandbox filtering or aggregation will substantially shrink the response. Use \`Promise.all\` or \`connecta.batch\` for independent calls inside one execution.
25
+ Unknown addresses plus dependent calls: search inside the run, not in an outer \`search_tools\`. Parallelize independent calls with \`Promise.all\` or \`connecta.batch\`.
26
26
 
27
27
  Connector namespace calls and \`connecta.call\` use the same read-only gate and throw on downstream errors. Catch only failures the workflow can handle; let authorization failures return to the agent for recovery.
28
28
 
29
- Do not use code mode for one call, independent calls already handled by \`batch_call\`, or any tool lacking \`readOnlyHint: true\`. Host calls and time are bounded. Return only the reduced value the agent needs.
29
+ Skip code mode for one call, calls suited to \`batch_call\`, or tools lacking \`readOnlyHint: true\`. Return only the needed reduction.
30
30
  `;
31
31
 
32
32
  /**
package/src/types.ts CHANGED
@@ -276,8 +276,16 @@ export interface Connector {
276
276
  * pending URL could complete consent with their own account.
277
277
  */
278
278
  verifyState?(state: string | null, ctx: ConnectorContext): Promise<boolean>;
279
- /** Optional: complete a downstream OAuth flow (called by /oauth/callback/<id>). */
280
- finishAuth?(code: string, ctx: ConnectorContext): Promise<void>;
279
+ /**
280
+ * Optional: complete a downstream OAuth flow (called by
281
+ * /oauth/callback/<id>). `callbackParams` preserves the authorization
282
+ * server's RFC 9207 `iss` response parameter for SDK validation.
283
+ */
284
+ finishAuth?(
285
+ code: string,
286
+ ctx: ConnectorContext,
287
+ callbackParams?: URLSearchParams,
288
+ ): Promise<void>;
281
289
  /**
282
290
  * Optional: serve a connector-owned HTTP route — for example a signed
283
291
  * download link minted by one of the connector's tools. Called only after
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.8.1";
7
+ export const CONNECTA_VERSION = "0.9.1";