@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.
- package/CHANGELOG.md +41 -0
- package/SECURITY.md +5 -11
- package/dist/auth/downstream-oauth.d.ts +15 -6
- package/dist/auth/downstream-oauth.d.ts.map +1 -1
- package/dist/auth/downstream-oauth.js +60 -11
- package/dist/auth/downstream-oauth.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +65 -50
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +3 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +38 -7
- package/dist/execute.js.map +1 -1
- package/dist/meta-tools.d.ts +1 -1
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +17 -15
- package/dist/meta-tools.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +63 -44
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/oauth.js +1 -1
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/shared.d.ts +2 -2
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/auth/downstream-oauth.ts +106 -24
- package/src/connectors/remote-mcp.ts +96 -64
- package/src/errors.ts +2 -0
- package/src/execute.ts +40 -6
- package/src/meta-tools.ts +20 -16
- package/src/routes/mcp.ts +70 -44
- package/src/routes/oauth.ts +1 -1
- package/src/routes/shared.ts +2 -2
- package/src/types.ts +10 -2
- package/src/version.ts +1 -1
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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/
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
566
|
+
});
|
|
560
567
|
}
|
|
561
568
|
const headers =
|
|
562
569
|
opts.auth?.type === "headers" ? opts.auth.headers : undefined;
|
|
563
|
-
return new StreamableHTTPClientTransport(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
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
|
-
{
|
|
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
|
-
//
|
|
875
|
-
//
|
|
876
|
-
|
|
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
|
-
|
|
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
|
-
|
|
915
|
-
|
|
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
|
-
|
|
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/
|
|
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)
|
|
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)
|
|
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/
|
|
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 (
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
:
|
|
216
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/routes/oauth.ts
CHANGED
|
@@ -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.`,
|