@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +252 -0
  2. package/README.md +72 -17
  3. package/SECURITY.md +10 -6
  4. package/dist/activity.d.ts +8 -0
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js +1 -0
  7. package/dist/activity.js.map +1 -1
  8. package/dist/connectors/api.d.ts +23 -0
  9. package/dist/connectors/api.d.ts.map +1 -1
  10. package/dist/connectors/api.js +13 -1
  11. package/dist/connectors/api.js.map +1 -1
  12. package/dist/connectors/remote-mcp.d.ts +27 -1
  13. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  14. package/dist/connectors/remote-mcp.js +31 -0
  15. package/dist/connectors/remote-mcp.js.map +1 -1
  16. package/dist/credentials.d.ts +2 -1
  17. package/dist/credentials.d.ts.map +1 -1
  18. package/dist/credentials.js +4 -2
  19. package/dist/credentials.js.map +1 -1
  20. package/dist/execute.d.ts +4 -4
  21. package/dist/execute.d.ts.map +1 -1
  22. package/dist/execute.js.map +1 -1
  23. package/dist/executors/quickjs.d.ts.map +1 -1
  24. package/dist/executors/quickjs.js +32 -4
  25. package/dist/executors/quickjs.js.map +1 -1
  26. package/dist/index.d.ts +51 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +67 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/meta-tools.d.ts +25 -4
  31. package/dist/meta-tools.d.ts.map +1 -1
  32. package/dist/meta-tools.js +138 -24
  33. package/dist/meta-tools.js.map +1 -1
  34. package/dist/registry.d.ts +183 -2
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +293 -27
  37. package/dist/registry.js.map +1 -1
  38. package/dist/server.d.ts +9 -1
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +96 -12
  41. package/dist/server.js.map +1 -1
  42. package/dist/skills.d.ts +52 -1
  43. package/dist/skills.d.ts.map +1 -1
  44. package/dist/skills.js +161 -1
  45. package/dist/skills.js.map +1 -1
  46. package/dist/storage/file.d.ts.map +1 -1
  47. package/dist/storage/file.js +19 -3
  48. package/dist/storage/file.js.map +1 -1
  49. package/dist/toolkits.d.ts +44 -0
  50. package/dist/toolkits.d.ts.map +1 -0
  51. package/dist/toolkits.js +134 -0
  52. package/dist/toolkits.js.map +1 -0
  53. package/dist/types.d.ts +20 -1
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/ui.d.ts +29 -1
  56. package/dist/ui.d.ts.map +1 -1
  57. package/dist/ui.js +100 -15
  58. package/dist/ui.js.map +1 -1
  59. package/dist/validate.d.ts +33 -1
  60. package/dist/validate.d.ts.map +1 -1
  61. package/dist/validate.js +32 -2
  62. package/dist/validate.js.map +1 -1
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +5 -2
  66. package/src/activity.ts +9 -0
  67. package/src/connectors/api.ts +36 -1
  68. package/src/connectors/remote-mcp.ts +67 -0
  69. package/src/credentials.ts +6 -3
  70. package/src/execute.ts +4 -4
  71. package/src/executors/quickjs.ts +32 -4
  72. package/src/index.ts +141 -2
  73. package/src/meta-tools.ts +215 -40
  74. package/src/registry.ts +416 -29
  75. package/src/server.ts +130 -12
  76. package/src/skills.ts +184 -1
  77. package/src/storage/file.ts +18 -2
  78. package/src/toolkits.ts +215 -0
  79. package/src/types.ts +20 -1
  80. package/src/ui.ts +103 -14
  81. package/src/validate.ts +60 -2
  82. package/src/version.ts +1 -1
@@ -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
@@ -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;AAsBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,wBAAwB,GAC7B,kBAAkB,GAAG,IAAI,CA+B3B"}
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?.validate(args);
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
@@ -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;AAgBjD,8EAA8E;AAC9E,uEAAuE;AACvE,0EAA0E;AAC1E,kEAAkE;AAClE,MAAM,UAAU,GAAG,IAAI,OAAO,EAAgC,CAAC;AAE/D,SAAS,iBAAiB,CACxB,MAAkB,EAClB,OAAe,EACf,MAAc,EACd,GAAY;IAEZ,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC7B,MAAM,CAAC,IAAI,CACT,oBAAoB,OAAO,kDACzB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,iCAAiC,CAClC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAkB,EAClB,IAAa,EACb,IAA8B;IAE9B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC;IACtC,IAAI,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,SAAS,GAAG,IAAI,SAAS,CAAC,MAAe,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;YAC7D,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;YACrD,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,uEAAuE;QACvE,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,KAAK,GAAG,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;aACtD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,gBAAgB,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC;aAC/C,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,OAAO,IAAI,kBAAkB,CAC3B,cAAc,EACd,0BAA0B,IAAI,CAAC,OAAO,MAAM,MAAM,IAAI,6CAA6C,EAAE,CACtG,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,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.4.0";
7
+ export declare const CONNECTA_VERSION = "0.5.0";
8
8
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js 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 const CONNECTA_VERSION = "0.4.0";
7
+ export const CONNECTA_VERSION = "0.5.0";
8
8
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.4.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=high",
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);
@@ -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);
@@ -21,7 +21,8 @@ interface CredentialPlaintext {
21
21
 
22
22
  export interface CredentialFieldMetadata {
23
23
  configured: true;
24
- lastFour: string;
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
- lastFour: value.slice(-4),
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 { Registry } from "./registry.js";
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: 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: 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: Registry,
530
+ registry: RegistryView,
531
531
  ctx: {
532
532
  baseUrl: string;
533
533
  executor: Executor;
@@ -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 < MAX_LOG_ENTRIES) {
128
- logs.push(ctx.getString(h));
129
- } else if (logs.length === MAX_LOG_ENTRIES) {
130
- logs.push(`[log truncated after ${MAX_LOG_ENTRIES} entries]`);
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();