@zackbart/connecta 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +252 -0
- package/README.md +72 -17
- package/SECURITY.md +10 -6
- package/dist/activity.d.ts +8 -0
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +1 -0
- package/dist/activity.js.map +1 -1
- package/dist/connectors/api.d.ts +23 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +13 -1
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +27 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +31 -0
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credentials.d.ts +2 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +4 -2
- package/dist/credentials.js.map +1 -1
- package/dist/execute.d.ts +4 -4
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +32 -4
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -1
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +25 -4
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +138 -24
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +183 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +293 -27
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +96 -12
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +52 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +161 -1
- package/dist/skills.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +19 -3
- package/dist/storage/file.js.map +1 -1
- package/dist/toolkits.d.ts +44 -0
- package/dist/toolkits.d.ts.map +1 -0
- package/dist/toolkits.js +134 -0
- package/dist/toolkits.js.map +1 -0
- package/dist/types.d.ts +20 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +29 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +100 -15
- package/dist/ui.js.map +1 -1
- package/dist/validate.d.ts +33 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +32 -2
- package/dist/validate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/src/activity.ts +9 -0
- package/src/connectors/api.ts +36 -1
- package/src/connectors/remote-mcp.ts +67 -0
- package/src/credentials.ts +6 -3
- package/src/execute.ts +4 -4
- package/src/executors/quickjs.ts +32 -4
- package/src/index.ts +141 -2
- package/src/meta-tools.ts +215 -40
- package/src/registry.ts +416 -29
- package/src/server.ts +130 -12
- package/src/skills.ts +184 -1
- package/src/storage/file.ts +18 -2
- package/src/toolkits.ts +215 -0
- package/src/types.ts +20 -1
- package/src/ui.ts +103 -14
- package/src/validate.ts +60 -2
- package/src/version.ts +1 -1
package/dist/validate.d.ts
CHANGED
|
@@ -11,6 +11,27 @@ export interface ValidateToolInputOptions {
|
|
|
11
11
|
* unusable. Default console.
|
|
12
12
|
*/
|
|
13
13
|
logger?: Logger;
|
|
14
|
+
/**
|
|
15
|
+
* Fail-closed on a schema the validator cannot evaluate (default false =
|
|
16
|
+
* today's fail-open behavior). When true, a schema that cannot be compiled —
|
|
17
|
+
* or that only fails on first use, e.g. an unresolvable `$ref` — yields a
|
|
18
|
+
* non-retryable `invalid_args` error instead of passing the raw arguments
|
|
19
|
+
* through, so unvalidated input is never silently admitted. The happy path
|
|
20
|
+
* (a schema that compiles and validates) is unaffected.
|
|
21
|
+
*/
|
|
22
|
+
failClosed?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface PrecompileValidatorOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Tool address used in the warning text, conventionally
|
|
27
|
+
* `"connectorId.toolName"`.
|
|
28
|
+
*/
|
|
29
|
+
address: string;
|
|
30
|
+
/**
|
|
31
|
+
* Destination for the warning emitted when the schema cannot be compiled.
|
|
32
|
+
* Default console.
|
|
33
|
+
*/
|
|
34
|
+
logger?: Logger;
|
|
14
35
|
}
|
|
15
36
|
/**
|
|
16
37
|
* Validate call arguments against a tool's JSON Schema.
|
|
@@ -24,7 +45,9 @@ export interface ValidateToolInputOptions {
|
|
|
24
45
|
*
|
|
25
46
|
* A schema the validator cannot compile (or that only fails on first use, e.g.
|
|
26
47
|
* an unresolvable `$ref`) is warned about once and then passed through — a
|
|
27
|
-
* broken schema should not break an otherwise working tool.
|
|
48
|
+
* broken schema should not break an otherwise working tool. Pass
|
|
49
|
+
* `failClosed: true` to instead reject such calls with `invalid_args`, for
|
|
50
|
+
* callers that would rather refuse a call than forward unvalidated arguments.
|
|
28
51
|
*
|
|
29
52
|
* The compiled validator is cached by **schema object identity**, so pass a
|
|
30
53
|
* stable object: hold the parsed manifest and hand the same schema back on
|
|
@@ -36,4 +59,13 @@ export interface ValidateToolInputOptions {
|
|
|
36
59
|
* the `Connector` interface directly.
|
|
37
60
|
*/
|
|
38
61
|
export declare function validateToolInput(schema: JsonSchema, args: unknown, opts: ValidateToolInputOptions): ConnectorCallError | null;
|
|
62
|
+
/**
|
|
63
|
+
* Eagerly compile and cache a tool's inputSchema so a schema the validator
|
|
64
|
+
* cannot use surfaces once at connector construction rather than silently on
|
|
65
|
+
* the first call. Reuses the same module-level cache `validateToolInput` reads,
|
|
66
|
+
* so the runtime path hits the cache. Warning-only: it never throws and never
|
|
67
|
+
* changes call behavior. A schema that only fails on first `validate()` (e.g.
|
|
68
|
+
* an unresolvable `$ref`) still slips through here and is caught at call time.
|
|
69
|
+
*/
|
|
70
|
+
export declare function precompileValidator(schema: JsonSchema, opts: PrecompileValidatorOptions): void;
|
|
39
71
|
//# sourceMappingURL=validate.d.ts.map
|
package/dist/validate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA6BD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,wBAAwB,GAC7B,kBAAkB,GAAG,IAAI,CAqC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,0BAA0B,GAC/B,IAAI,CAQN"}
|
package/dist/validate.js
CHANGED
|
@@ -5,6 +5,9 @@ import { ConnectorCallError } from "./errors.js";
|
|
|
5
5
|
// breaking a working tool). A WeakMap so schemas belonging to a discarded
|
|
6
6
|
// connector are collectable, the same pattern compactSchema uses.
|
|
7
7
|
const validators = new WeakMap();
|
|
8
|
+
function unevaluableSchema(address) {
|
|
9
|
+
return new ConnectorCallError("invalid_args", `Cannot validate arguments for "${address}": its inputSchema could not be evaluated`);
|
|
10
|
+
}
|
|
8
11
|
function disableValidation(schema, address, logger, err) {
|
|
9
12
|
validators.set(schema, null);
|
|
10
13
|
logger.warn(`[connecta] tool "${address}" has an inputSchema the validator cannot use (${err instanceof Error ? err.message : String(err)}) — arguments are not validated`);
|
|
@@ -21,7 +24,9 @@ function disableValidation(schema, address, logger, err) {
|
|
|
21
24
|
*
|
|
22
25
|
* A schema the validator cannot compile (or that only fails on first use, e.g.
|
|
23
26
|
* an unresolvable `$ref`) is warned about once and then passed through — a
|
|
24
|
-
* broken schema should not break an otherwise working tool.
|
|
27
|
+
* broken schema should not break an otherwise working tool. Pass
|
|
28
|
+
* `failClosed: true` to instead reject such calls with `invalid_args`, for
|
|
29
|
+
* callers that would rather refuse a call than forward unvalidated arguments.
|
|
25
30
|
*
|
|
26
31
|
* The compiled validator is cached by **schema object identity**, so pass a
|
|
27
32
|
* stable object: hold the parsed manifest and hand the same schema back on
|
|
@@ -45,13 +50,19 @@ export function validateToolInput(schema, args, opts) {
|
|
|
45
50
|
validator = null;
|
|
46
51
|
}
|
|
47
52
|
}
|
|
53
|
+
// A schema the validator could not compile (or that a prior call disabled):
|
|
54
|
+
// pass through by default, refuse when the caller opted into fail-closed.
|
|
55
|
+
if (validator === null) {
|
|
56
|
+
return opts.failClosed ? unevaluableSchema(opts.address) : null;
|
|
57
|
+
}
|
|
48
58
|
let result;
|
|
49
59
|
try {
|
|
50
|
-
result = validator
|
|
60
|
+
result = validator.validate(args);
|
|
51
61
|
}
|
|
52
62
|
catch (err) {
|
|
53
63
|
// e.g. an unresolvable $ref — surfaces on first validate, not compile.
|
|
54
64
|
disableValidation(schema, opts.address, logger, err);
|
|
65
|
+
return opts.failClosed ? unevaluableSchema(opts.address) : null;
|
|
55
66
|
}
|
|
56
67
|
if (result && !result.valid) {
|
|
57
68
|
const units = result.errors.filter((u) => u.instanceLocation !== "#");
|
|
@@ -63,4 +74,23 @@ export function validateToolInput(schema, args, opts) {
|
|
|
63
74
|
}
|
|
64
75
|
return null;
|
|
65
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Eagerly compile and cache a tool's inputSchema so a schema the validator
|
|
79
|
+
* cannot use surfaces once at connector construction rather than silently on
|
|
80
|
+
* the first call. Reuses the same module-level cache `validateToolInput` reads,
|
|
81
|
+
* so the runtime path hits the cache. Warning-only: it never throws and never
|
|
82
|
+
* changes call behavior. A schema that only fails on first `validate()` (e.g.
|
|
83
|
+
* an unresolvable `$ref`) still slips through here and is caught at call time.
|
|
84
|
+
*/
|
|
85
|
+
export function precompileValidator(schema, opts) {
|
|
86
|
+
if (validators.has(schema))
|
|
87
|
+
return;
|
|
88
|
+
const logger = opts.logger ?? console;
|
|
89
|
+
try {
|
|
90
|
+
validators.set(schema, new Validator(schema, "2020-12", false));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
disableValidation(schema, opts.address, logger, err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
66
96
|
//# sourceMappingURL=validate.js.map
|
package/dist/validate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
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 declare const CONNECTA_VERSION = "0.
|
|
7
|
+
export declare const CONNECTA_VERSION = "0.5.0";
|
|
8
8
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind nine meta-tools.",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"test:node": "vitest run --project node",
|
|
65
65
|
"test:workers": "vitest run --project workers",
|
|
66
66
|
"check:examples": "tsc -p tsconfig.examples.node.json && tsc -p tsconfig.examples.worker.json",
|
|
67
|
-
"check:security": "npm audit --omit=dev --audit-level=
|
|
67
|
+
"check:security": "npm audit --omit=dev --audit-level=moderate",
|
|
68
68
|
"check:package": "node scripts/check-package.mjs",
|
|
69
69
|
"check": "npm run typecheck && npm run test && npm run build && npm run check:examples",
|
|
70
70
|
"release:check": "npm run check && npm run check:security && npm run check:package",
|
|
@@ -76,6 +76,9 @@
|
|
|
76
76
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
77
77
|
"zod": "^4.4.3"
|
|
78
78
|
},
|
|
79
|
+
"overrides": {
|
|
80
|
+
"@hono/node-server": "^2.0.12"
|
|
81
|
+
},
|
|
79
82
|
"peerDependencies": {
|
|
80
83
|
"@clerk/backend": "^3.12.0",
|
|
81
84
|
"quickjs-emscripten": "^0.32.0"
|
package/src/activity.ts
CHANGED
|
@@ -42,6 +42,12 @@ export interface ToolCallActivityEvent {
|
|
|
42
42
|
serverName: string;
|
|
43
43
|
serverVersion: string;
|
|
44
44
|
deploymentId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Name of the toolkit this connection selected with `?toolkit=`, when it
|
|
47
|
+
* selected one. Calls are recorded the same way with or without it; this only
|
|
48
|
+
* says which scoped view the call came through.
|
|
49
|
+
*/
|
|
50
|
+
toolkitId?: string;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export interface ActivityPage {
|
|
@@ -83,6 +89,8 @@ export interface ActivityRequestContext {
|
|
|
83
89
|
requestId: string;
|
|
84
90
|
serverInfo: { name: string; version: string };
|
|
85
91
|
deploymentId?: string;
|
|
92
|
+
/** Toolkit this connection is scoped to, when `?toolkit=` selected one. */
|
|
93
|
+
toolkitId?: string;
|
|
86
94
|
defer?: (promise: Promise<unknown>) => void;
|
|
87
95
|
logger: Logger;
|
|
88
96
|
}
|
|
@@ -128,6 +136,7 @@ export function recordToolActivity(
|
|
|
128
136
|
...(context.deploymentId
|
|
129
137
|
? { deploymentId: context.deploymentId }
|
|
130
138
|
: {}),
|
|
139
|
+
...(context.toolkitId ? { toolkitId: context.toolkitId } : {}),
|
|
131
140
|
};
|
|
132
141
|
try {
|
|
133
142
|
const result = context.sink.record(event);
|
package/src/connectors/api.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateToolInput } from "../validate.js";
|
|
1
|
+
import { precompileValidator, validateToolInput } from "../validate.js";
|
|
2
2
|
import type {
|
|
3
3
|
Connector,
|
|
4
4
|
ConnectorCredentialConfig,
|
|
@@ -29,6 +29,19 @@ export interface ApiOptions {
|
|
|
29
29
|
/** Human-readable display name; the connector id remains the address prefix. */
|
|
30
30
|
title?: string;
|
|
31
31
|
description?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Max inline result size (bytes) for this connector's tools before
|
|
34
|
+
* call_tool/batch_call truncate and stash the full text for get_result
|
|
35
|
+
* paging. Overrides the deployment's `maxResultBytes`; omit to inherit it.
|
|
36
|
+
* Must be a whole number of bytes >= 1; anything else warns at startup and
|
|
37
|
+
* is ignored.
|
|
38
|
+
*/
|
|
39
|
+
maxResultBytes?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Optional agent-facing usage guide (markdown) served by the `skills`
|
|
42
|
+
* meta-tool as `connector:<id>`. See `Connector.usageGuide`.
|
|
43
|
+
*/
|
|
44
|
+
usageGuide?: string;
|
|
32
45
|
/** Optional operator-managed credential exposed through ctx.credential and /ui. */
|
|
33
46
|
credential?: ConnectorCredentialConfig;
|
|
34
47
|
/** Optional validation behind /ui's Test action. */
|
|
@@ -49,6 +62,16 @@ export interface ApiOptions {
|
|
|
49
62
|
* on loose coercion.
|
|
50
63
|
*/
|
|
51
64
|
validateArgs?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Fail-closed on a tool whose `inputSchema` the validator cannot evaluate
|
|
67
|
+
* (default false). The default surfaces such a schema as a one-time warning
|
|
68
|
+
* and then passes the raw arguments through, so a broken schema never breaks
|
|
69
|
+
* an otherwise working tool. Set true to instead reject those calls with a
|
|
70
|
+
* non-retryable `invalid_args` ConnectorCallError, so a schema that cannot be
|
|
71
|
+
* enforced never silently admits unvalidated input. Only consulted when
|
|
72
|
+
* `validateArgs` is not false.
|
|
73
|
+
*/
|
|
74
|
+
strictValidation?: boolean;
|
|
52
75
|
tools: ApiTool[];
|
|
53
76
|
}
|
|
54
77
|
|
|
@@ -73,11 +96,22 @@ export function api(id: string, opts: ApiOptions): Connector {
|
|
|
73
96
|
}));
|
|
74
97
|
const byName = new Map(opts.tools.map((t) => [t.name, t]));
|
|
75
98
|
const validateArgs = opts.validateArgs ?? true;
|
|
99
|
+
const strictValidation = opts.strictValidation ?? false;
|
|
100
|
+
if (validateArgs) {
|
|
101
|
+
// Compile each schema now so a validator-hostile inputSchema surfaces once
|
|
102
|
+
// here rather than silently on its first call. Warning-only; never throws.
|
|
103
|
+
for (const t of opts.tools) {
|
|
104
|
+
if (t.inputSchema)
|
|
105
|
+
precompileValidator(t.inputSchema, { address: `${id}.${t.name}` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
76
108
|
return {
|
|
77
109
|
id,
|
|
78
110
|
title: opts.title,
|
|
79
111
|
kind: "api",
|
|
80
112
|
description: opts.description,
|
|
113
|
+
maxResultBytes: opts.maxResultBytes,
|
|
114
|
+
usageGuide: opts.usageGuide,
|
|
81
115
|
credential: opts.credential,
|
|
82
116
|
testCredential: opts.testCredential,
|
|
83
117
|
testCredentials: opts.testCredentials,
|
|
@@ -95,6 +129,7 @@ export function api(id: string, opts: ApiOptions): Connector {
|
|
|
95
129
|
const invalid = validateToolInput(tool.inputSchema, input, {
|
|
96
130
|
address: `${id}.${name}`,
|
|
97
131
|
logger: ctx.logger,
|
|
132
|
+
failClosed: strictValidation,
|
|
98
133
|
});
|
|
99
134
|
if (invalid) throw invalid;
|
|
100
135
|
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
Connector,
|
|
11
11
|
ConnectorContext,
|
|
12
12
|
ConnectorStatus,
|
|
13
|
+
Logger,
|
|
13
14
|
ToolDef,
|
|
14
15
|
} from "../types.js";
|
|
15
16
|
|
|
@@ -22,7 +23,33 @@ export interface RemoteMcpOptions {
|
|
|
22
23
|
/** Human-readable display name; the connector id remains the address prefix. */
|
|
23
24
|
title?: string;
|
|
24
25
|
description?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Max inline result size (bytes) for this connector's tools before
|
|
28
|
+
* call_tool/batch_call truncate and stash the full text for get_result
|
|
29
|
+
* paging. Overrides the deployment's `maxResultBytes`; omit to inherit it.
|
|
30
|
+
* Must be a whole number of bytes >= 1; anything else warns at startup and
|
|
31
|
+
* is ignored.
|
|
32
|
+
*/
|
|
33
|
+
maxResultBytes?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Optional agent-facing usage guide (markdown) served by the `skills`
|
|
36
|
+
* meta-tool as `connector:<id>`. See `Connector.usageGuide`.
|
|
37
|
+
*/
|
|
38
|
+
usageGuide?: string;
|
|
25
39
|
auth?: RemoteMcpAuth;
|
|
40
|
+
/**
|
|
41
|
+
* Refuse to connect to a non-`https://` `url` at construction (default
|
|
42
|
+
* false). Loopback hosts (`localhost`, `127.0.0.1`, `[::1]`) are always
|
|
43
|
+
* allowed for local development. Off by default, static `headers` credentials
|
|
44
|
+
* over a cleartext connection are warned about but permitted; set this true
|
|
45
|
+
* to make that misconfiguration a hard error instead.
|
|
46
|
+
*/
|
|
47
|
+
requireHttps?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Destination for the cleartext-credential warning emitted at construction.
|
|
50
|
+
* Default console.
|
|
51
|
+
*/
|
|
52
|
+
logger?: Logger;
|
|
26
53
|
/**
|
|
27
54
|
* @internal Testing seam. When set, this transport is used instead of the
|
|
28
55
|
* HTTP transport, letting tests point the connector at an in-process MCP
|
|
@@ -35,6 +62,15 @@ function msg(err: unknown): string {
|
|
|
35
62
|
return err instanceof Error ? err.message : String(err);
|
|
36
63
|
}
|
|
37
64
|
|
|
65
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
66
|
+
return (
|
|
67
|
+
hostname === "localhost" ||
|
|
68
|
+
hostname === "127.0.0.1" ||
|
|
69
|
+
hostname === "[::1]" ||
|
|
70
|
+
hostname === "::1"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
38
74
|
interface ConnectionState {
|
|
39
75
|
client: Client | null;
|
|
40
76
|
transport: Transport | null;
|
|
@@ -60,6 +96,27 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
60
96
|
// from the isolate singleton. Those are request-bound in Cloudflare Workers.
|
|
61
97
|
const states = new WeakMap<object, ConnectionState>();
|
|
62
98
|
const isOauth = opts.auth?.type === "oauth";
|
|
99
|
+
const logger = opts.logger ?? console;
|
|
100
|
+
|
|
101
|
+
// Check the destination scheme once at construction: buildTransport (and the
|
|
102
|
+
// SDK's fetch) attach any static credentials to every request, so an http://
|
|
103
|
+
// endpoint sends bearer tokens / API keys in cleartext. Loopback is exempt
|
|
104
|
+
// for local development.
|
|
105
|
+
const destination = new URL(opts.url);
|
|
106
|
+
const insecureDestination =
|
|
107
|
+
destination.protocol !== "https:" && !isLoopbackHost(destination.hostname);
|
|
108
|
+
if (insecureDestination) {
|
|
109
|
+
if (opts.requireHttps) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`[connecta] connector "${id}" url ${opts.url} is not https:// (and not loopback) — refusing to connect (requireHttps).`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (opts.auth?.type === "headers") {
|
|
115
|
+
logger.warn(
|
|
116
|
+
`[connecta] connector "${id}" sends static credentials to ${opts.url} over a non-https:// connection — those tokens will be transmitted in cleartext.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
63
120
|
|
|
64
121
|
/** Typed per-call auth signal; the SDK's UnauthorizedError stays as cause. */
|
|
65
122
|
const authRequiredError = (cause: unknown) =>
|
|
@@ -98,6 +155,14 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
98
155
|
return state.provider;
|
|
99
156
|
};
|
|
100
157
|
|
|
158
|
+
// NOTE: StreamableHTTPClientTransport speaks over fetch, which transparently
|
|
159
|
+
// follows 3xx redirects. A malicious or compromised downstream MCP could
|
|
160
|
+
// redirect to an internal address (e.g. http://169.254.169.254/…) and fetch
|
|
161
|
+
// would re-issue the request — potentially carrying static auth headers. The
|
|
162
|
+
// scheme check above only guards the first hop; a fully robust guard (manual
|
|
163
|
+
// redirect handling + per-hop re-validation + stripping auth headers cross-
|
|
164
|
+
// origin) lives in the SDK transport and is deferred to a future non-patch
|
|
165
|
+
// release rather than reimplemented here.
|
|
101
166
|
const buildTransport = (
|
|
102
167
|
ctx: ConnectorContext,
|
|
103
168
|
state: ConnectionState,
|
|
@@ -200,6 +265,8 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
200
265
|
title: opts.title,
|
|
201
266
|
kind: "mcp",
|
|
202
267
|
description: opts.description,
|
|
268
|
+
maxResultBytes: opts.maxResultBytes,
|
|
269
|
+
usageGuide: opts.usageGuide,
|
|
203
270
|
|
|
204
271
|
async listTools(ctx) {
|
|
205
272
|
const state = stateFor(ctx);
|
package/src/credentials.ts
CHANGED
|
@@ -21,7 +21,8 @@ interface CredentialPlaintext {
|
|
|
21
21
|
|
|
22
22
|
export interface CredentialFieldMetadata {
|
|
23
23
|
configured: true;
|
|
24
|
-
|
|
24
|
+
/** Only emitted when the value is long enough that four chars don't leak much. */
|
|
25
|
+
lastFour?: string;
|
|
25
26
|
updatedAt: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -200,7 +201,9 @@ export class CredentialVault {
|
|
|
200
201
|
field,
|
|
201
202
|
{
|
|
202
203
|
configured: true as const,
|
|
203
|
-
|
|
204
|
+
// Only reveal the tail on values comfortably longer than four chars;
|
|
205
|
+
// for a short secret those four would be half of it.
|
|
206
|
+
...(value.length >= 12 ? { lastFour: value.slice(-4) } : {}),
|
|
204
207
|
updatedAt: credential.updatedAt,
|
|
205
208
|
},
|
|
206
209
|
]),
|
|
@@ -208,7 +211,7 @@ export class CredentialVault {
|
|
|
208
211
|
const single = fields.value;
|
|
209
212
|
return {
|
|
210
213
|
configured: true,
|
|
211
|
-
...(single ? { lastFour: single.lastFour } : {}),
|
|
214
|
+
...(single?.lastFour ? { lastFour: single.lastFour } : {}),
|
|
212
215
|
updatedAt: credential.updatedAt,
|
|
213
216
|
fields,
|
|
214
217
|
};
|
package/src/execute.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
|
|
|
5
5
|
import { errorResult, jsonResult, type ToolResult } from "./meta-tools.js";
|
|
6
6
|
import { classifyCallError, ConnectorCallError } from "./errors.js";
|
|
7
7
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
8
|
-
import type {
|
|
8
|
+
import type { RegistryView } from "./registry.js";
|
|
9
9
|
import type {
|
|
10
10
|
Connector,
|
|
11
11
|
Executor,
|
|
@@ -98,7 +98,7 @@ export function unwrapForSandbox(
|
|
|
98
98
|
* the raw-address escape hatch. Broken connectors are skipped, not fatal.
|
|
99
99
|
*/
|
|
100
100
|
export async function buildSandboxProviders(
|
|
101
|
-
registry:
|
|
101
|
+
registry: RegistryView,
|
|
102
102
|
baseUrl: string,
|
|
103
103
|
logger: Logger,
|
|
104
104
|
activity?: ActivityRequestContext,
|
|
@@ -459,7 +459,7 @@ function guardResultValue(value: unknown): unknown {
|
|
|
459
459
|
|
|
460
460
|
/** The execute_code handler. Exported for direct testing. */
|
|
461
461
|
export function createExecuteTool(
|
|
462
|
-
registry:
|
|
462
|
+
registry: RegistryView,
|
|
463
463
|
baseUrl: string,
|
|
464
464
|
executor: Executor,
|
|
465
465
|
logger: Logger,
|
|
@@ -527,7 +527,7 @@ Example: async () => { const r = await crm.search({ query: "roadmap" }); return
|
|
|
527
527
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
528
528
|
export function registerExecuteTool(
|
|
529
529
|
server: McpServer,
|
|
530
|
-
registry:
|
|
530
|
+
registry: RegistryView,
|
|
531
531
|
ctx: {
|
|
532
532
|
baseUrl: string;
|
|
533
533
|
executor: Executor;
|
package/src/executors/quickjs.ts
CHANGED
|
@@ -32,6 +32,14 @@ export interface QuickJsExecutorOptions {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const MAX_LOG_ENTRIES = 200;
|
|
35
|
+
// Cap each entry AND the cumulative buffer at capture time so untrusted guest
|
|
36
|
+
// code can't retain unbounded host memory: a single `console.log("x".repeat(N))`
|
|
37
|
+
// otherwise copies the whole N-char guest string into a host array we hold for
|
|
38
|
+
// the entire execution. 8k chars/entry is generous for glue-code logging (the
|
|
39
|
+
// join in execute.ts trims the assembled log to 4k anyway), and 256k total
|
|
40
|
+
// keeps the worst case — 200 maxed-out entries — bounded well under a MiB.
|
|
41
|
+
const MAX_LOG_ENTRY_CHARS = 8_000;
|
|
42
|
+
const MAX_LOG_TOTAL_CHARS = 256_000;
|
|
35
43
|
|
|
36
44
|
function msg(err: unknown): string {
|
|
37
45
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -123,12 +131,32 @@ function installBridge(
|
|
|
123
131
|
};
|
|
124
132
|
armWake(bridge);
|
|
125
133
|
|
|
134
|
+
// Running total of chars actually retained in `logs`; once the cumulative
|
|
135
|
+
// budget is spent we push one marker and drop the rest, so a flood of large
|
|
136
|
+
// entries can't grow the host array without bound.
|
|
137
|
+
let logTotalChars = 0;
|
|
138
|
+
let logBudgetSpent = false;
|
|
126
139
|
const logFn = ctx.newFunction("__log", (h) => {
|
|
127
|
-
if (logs.length
|
|
128
|
-
logs.
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
if (logs.length >= MAX_LOG_ENTRIES) {
|
|
141
|
+
if (logs.length === MAX_LOG_ENTRIES) {
|
|
142
|
+
logs.push(`[log truncated after ${MAX_LOG_ENTRIES} entries]`);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (logBudgetSpent) return;
|
|
147
|
+
// Cap the entry before retaining it: `getString` yields a transient copy,
|
|
148
|
+
// but slicing here keeps only a bounded string alive in `logs`.
|
|
149
|
+
let entry = ctx.getString(h);
|
|
150
|
+
if (entry.length > MAX_LOG_ENTRY_CHARS) {
|
|
151
|
+
entry = `${entry.slice(0, MAX_LOG_ENTRY_CHARS)}…[entry truncated]`;
|
|
152
|
+
}
|
|
153
|
+
if (logTotalChars + entry.length > MAX_LOG_TOTAL_CHARS) {
|
|
154
|
+
logs.push("[log truncated: size budget exceeded]");
|
|
155
|
+
logBudgetSpent = true;
|
|
156
|
+
return;
|
|
131
157
|
}
|
|
158
|
+
logs.push(entry);
|
|
159
|
+
logTotalChars += entry.length;
|
|
132
160
|
});
|
|
133
161
|
ctx.setProp(ctx.global, "__log", logFn);
|
|
134
162
|
logFn.dispose();
|