@jini-ai/mcp 0.4.4 → 0.6.10

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 CHANGED
@@ -1,5 +1,77 @@
1
1
  # @jini-ai/mcp
2
2
 
3
+ ## 0.6.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 4f52784: Add a stable `./bin` export subpath resolving to the `jini-mcp` executable.
8
+
9
+ The package declared a `bin` entry for `dist/bin/serve.js` but an `exports` map containing only
10
+ `"."`, so a consumer that needed the executable's path in order to spawn it as a CLI-injected MCP
11
+ server could not ask for one: `require.resolve('@jini-ai/mcp/dist/bin/serve.js')` fails with
12
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`. Consumers were resolving the root export and walking to a sibling
13
+ path by hand, hardcoding this package's private build layout.
14
+
15
+ `require.resolve('@jini-ai/mcp/bin')` now returns that path directly. The subpath deliberately
16
+ declares only a `default` condition — it is meant to be resolved to a path and handed to `spawn`,
17
+ not imported (`serve.ts` uses top-level await behind its `isMainModule` guard, so it is not
18
+ `require()`-able). Purely additive: the `"."` export is unchanged.
19
+
20
+ - 8ff5653: Enforce a tool's declared `inputSchema` server-side before its handler runs.
21
+
22
+ `createMcpToolServer` wires tools through the MCP SDK's low-level `Server.setRequestHandler`, not
23
+ the schema-validating `McpServer.registerTool()` helper — the former never checked a real client's
24
+ `tools/call` arguments against the tool's own declared `inputSchema`. A wire-level test (a real SDK
25
+ `Client` and `Server` over `InMemoryTransport`, not the existing `FakeTransport` suite, which calls
26
+ handlers directly either way and so cannot see this) proved schema-violating arguments — wrong
27
+ type, an undeclared property under `additionalProperties: false` — reached every handler completely
28
+ unvalidated.
29
+
30
+ `handleToolCall` now validates `rawArgs` against `tool.inputSchema` via the MCP SDK's own
31
+ `@modelcontextprotocol/sdk/validation/cfworker` provider (no `eval`/codegen, matching this package's
32
+ Node CLI runtime) before invoking the handler, returning the same `{isError:true}` shape as an
33
+ unknown-tool-name or thrown-error result rather than ever calling the handler with unvalidated
34
+ input. Each tool's compiled validator is cached (`WeakMap` keyed by the `McpToolDef` instance, stable
35
+ for a server's whole lifetime) since compiling one is real work the SDK's validator does not cache
36
+ internally. Adds `@cfworker/json-schema` (already an optional peer of the installed SDK version) as
37
+ a direct dependency.
38
+
39
+ - 3fe4d14: Add default-off delegated-result v1 text markers for generic, read-only and typed gateways. The stdio host can opt in with JINI_DELEGATED_RESULT_MARKERS=1.
40
+ - 9098d22: Describe site-guide discovery as an optional host capability in the generic MCP instructions.
41
+ - 4f52784: Add a `sidecar-strict` security mode and per-run MCP credential propagation.
42
+
43
+ For a daemon whose threat model is **another process running as the same OS user** rather than a
44
+ remote attacker, the existing `jini-local` mode is a no-op: `registerApiBearerAuthMiddleware`
45
+ short-circuits for any loopback peer before it reads the `Authorization` header, and a `127.0.0.1`
46
+ bind keeps remote hosts out while doing nothing about a co-resident process. A consumer that spawns a
47
+ Jini daemon holding real authority — starting agent runs, executing tools against a real database —
48
+ previously had to write its own middleware.
49
+
50
+ - `@jini-ai/http-kit` gains `requireStrictBearerToken`: fail-closed 503 when the named token env var
51
+ is unset, 401 on mismatch, **no loopback exemption and no disable flag**. Its `tokenEnvVar` is
52
+ required with no default, so this package never names a host's secret.
53
+ - `composeJiniKernel` gains `security: { mode: 'sidecar-strict', host, tokenEnvVar, exemptPaths? }`.
54
+ Purely additive — `host` and `jini-local` are unchanged by construction, since the modes are arms of
55
+ a discriminated union. The strict gate mounts ahead of the JSON body parser, so a caller it rejects
56
+ never has its body parsed.
57
+ - `@jini-ai/daemon`'s `McpJsonInjectionOptions` gains `credential?: (runId) => string | Promise<string>`
58
+ — a **resolver, not a string**, because injection options are built once before any run exists and a
59
+ boot-wide shared secret would defeat the point of scoping a credential to a run. It is delivered to
60
+ the child as `JINI_DAEMON_TOKEN`.
61
+ - `@jini-ai/mcp`'s `jini-mcp` reads that variable and attaches `Authorization` to every daemon call.
62
+ Optional throughout: with no credential, request headers and `.mcp.json` output are byte-identical
63
+ to before.
64
+
65
+ Also generalized: both existing bearer gates now compare tokens in constant time (`timingSafeEqual`)
66
+ and share one header-parsing helper, closing a timing side channel and removing a duplicated regex.
67
+
68
+ - Updated dependencies [22f0619]
69
+ - Updated dependencies [82ed2d3]
70
+ - Updated dependencies [3fe4d14]
71
+ - Updated dependencies [5a3de86]
72
+ - @jini-ai/core@0.6.10
73
+ - @jini-ai/protocol@0.6.10
74
+
3
75
  ## 0.4.4 — 2026-10-22
4
76
 
5
77
  - 2026-10-22: Typed tools from the launched bridge, concrete describe schemas, list-changed, diagnostic error codes, coverage.
package/dist/bin/serve.js CHANGED
@@ -148,6 +148,7 @@ function buildDelegatedGatewayOptions({ env }, deps) {
148
148
  const rawTimeout = env[DELEGATED_TOOL_TIMEOUT_ENV_VAR];
149
149
  return definedOnly({
150
150
  generateToolUseId: deps.generateToolUseId,
151
+ delegatedResultMarkers: env.JINI_DELEGATED_RESULT_MARKERS === '1' ? true : undefined,
151
152
  // Parsed, not validated: `createExecuteDelegatedToolTool` already rejects a non-finite or
152
153
  // non-positive value back to the default, so `NaN` from a malformed env var lands there too.
153
154
  delegatedToolTimeoutMs: rawTimeout === undefined ? undefined : Number(rawTimeout),
@@ -334,17 +334,17 @@ export function writeShapedSchemaInputNames({ schema }) {
334
334
  walk(schema, 0);
335
335
  return sortedUnique(found);
336
336
  }
337
- /** R3 action-name rules, applied to the snake_cased remote name. Kept as named module constants
337
+ /** R3 action-name rules, applied to the snake_cased remote name (camelCase split, `-` read as `_`). Kept as named module constants
338
338
  * (none carries the `g` flag, so `.test` is stateless) so {@link federatedCallConfirmationForAction}
339
339
  * reads as the ordered policy it is. */
340
- const DELIVERY_ACTION = /(?:send|deliver|invite|reply|forward).*(?:email|mail|newsletter|message|sms|invitation)|(?:email|mail|newsletter|message|sms).*(?:send|deliver|reply|forward)|(?:post|publish).*(?:message|newsletter)/;
340
+ const DELIVERY_ACTION = /(?:send|deliver|invite|reply|forward|broadcast).*(?:email|mail|newsletter|message|sms|invitation|broadcast)|(?:email|mail|newsletter|message|sms).*(?:send|deliver|reply|forward)|(?:post|publish).*(?:message|newsletter)/;
341
341
  const SYSTEM_PROMPT_ACTION = /(?:set|update|edit|change).*(?:system_prompt|system_instruction)/;
342
342
  const ASSISTANT_PREFIX = /^(?:assistant|agent)_/;
343
343
  const READ_ACTION = /^(?:get|list|read|search|describe)_/;
344
344
  const ASSISTANT_ACCESS_ACTION = /(?:assistant|agent).*(?:instruction|privacy|permission|access|policy)/;
345
345
  const SQL_ACTION = /(?:execute_sql|apply_migration|run_query|query_database)/;
346
346
  const REVERSIBLE_REMOVAL_ACTION = /(?:^|_)(?:trash|archive|tombstone|unpublish)(?:_|$)/;
347
- const PERMANENT_REMOVAL_ACTION = /(?:^|_)(?:delete|purge|drop|destroy|truncate)(?:_|$)/;
347
+ const PERMANENT_REMOVAL_ACTION = /(?:^|_)(?:delete|purge|drop|destroy|truncate|remove|revoke)(?:_|$)/;
348
348
  /**
349
349
  * Owner confirmation policy (2026-10-01). Ordinary writes run immediately; permanent deletion,
350
350
  * delivery to people and changes to the assistant's own access or instructions require a card.
@@ -358,7 +358,8 @@ const PERMANENT_REMOVAL_ACTION = /(?:^|_)(?:delete|purge|drop|destroy|truncate)(
358
358
  * @complexity O(n) in the serialized input size.
359
359
  */
360
360
  export function federatedCallConfirmationForAction({ remoteName, annotations, args }) {
361
- const name = remoteName.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
361
+ // `-` separates words like `_` does (`remove-domain`, `send-broadcast`): MCP servers use both.
362
+ const name = remoteName.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().replace(/-/g, "_");
362
363
  const declaredDestructive = annotations?.destructiveHint === true;
363
364
  if (DELIVERY_ACTION.test(name) || SYSTEM_PROMPT_ACTION.test(name))
364
365
  return "confirm";
@@ -1,38 +1,3 @@
1
- /**
2
- * @module @jini-ai/mcp/server/tools/delegated-tool
3
- *
4
- * `execute_delegated_tool` and its read-only companion `execute_readonly_delegated_tool` — the
5
- * MCP-callback half of gap 3's continuation transport (see
6
- * `packages/daemon/archived provenance ledger`'s "run/chat orchestration gap 3, part 1" addition and this
7
- * package's own dated section in `archived provenance ledger` for the rest of the spike). The swarm-consensus
8
- * Final Recommendation asked for exactly this: "inject the already-shipped MCP host into one
9
- * MCP-capable CLI's launch config, prove a tool round-trip through the existing
10
- * `delegated-tool-bridge.ts`." Where `../tools/run-tools.js`'s tools proxy generic
11
- * run-lifecycle HTTP endpoints, this tool proxies the new `POST /api/delegated-tool-calls`
12
- * daemon route (`packages/http/src/delegated-tools.ts`), which is the one and only thing that
13
- * calls into `createDelegatedToolBridge` (`packages/daemon/src/delegated-tool-bridge.ts`) — the
14
- * same `ToolExecutor` deny-by-default gate every other tool-execution path in this codebase
15
- * already uses. No parallel authorization mechanism is introduced anywhere on this path.
16
- *
17
- * Unlike every other tool this package ships (`run-tools.ts`'s five tools, plain static
18
- * objects), this one is a **factory**. The MCP server subprocess a `claude` run spawns via
19
- * `.mcp.json` (see `../../bin/serve.js`) is scoped to exactly one `runId` for its entire
20
- * lifetime — the daemon injected that run id into the subprocess's own environment at spawn
21
- * time, before the model ever said a word. `runId` is therefore closed over at construction
22
- * time, not accepted as a per-call tool argument: a model has no legitimate way to pick, or
23
- * need to know, which run it is currently inside, and letting it supply one would open a
24
- * confused-deputy path (one run's MCP subprocess executing a tool call "as" a different run).
25
- * `toolUseId` is likewise generated per call (`randomUUID` by default), not model-supplied — a
26
- * model has no legitimate reason to choose its own correlation id either.
27
- *
28
- * TWO defs, ONE code path. `createDelegatedGateway` builds both; the factories differ only in the
29
- * name/title/description they present and in whether the request carries `requireReadOnly: true`.
30
- * The read-only companion exists because MCP annotations are static per tool declaration — a
31
- * surface that promises "this only reads" can only be a second declaration, never a per-call mode —
32
- * and it ENFORCES that promise daemon-side rather than asserting it, because a `readOnlyHint: true`
33
- * tool that could still reach writes would launder a write past its caller's own safety gate. See
34
- * {@link createExecuteReadonlyDelegatedToolTool} for the failure that motivated it.
35
- */
36
1
  import { type ExposedToolDescriptor } from '@jini-ai/core';
37
2
  import { type McpToolDef } from '../tool-protocol.js';
38
3
  /**
@@ -82,6 +47,8 @@ export interface CreateExecuteDelegatedToolToolOptions {
82
47
  * @default DEFAULT_DELEGATED_TOOL_TIMEOUT_MS (6 min)
83
48
  */
84
49
  readonly delegatedToolTimeoutMs?: number;
50
+ /** Emit v1 metadata only when the host has enabled daemon/runtime echo correlation. Default off. */
51
+ readonly delegatedResultMarkers?: boolean;
85
52
  }
86
53
  /**
87
54
  * Builds the `execute_delegated_tool` tool def for one specific `runId`. A fresh MCP server
@@ -33,6 +33,7 @@
33
33
  * tool that could still reach writes would launder a write past its caller's own safety gate. See
34
34
  * {@link createExecuteReadonlyDelegatedToolTool} for the failure that motivated it.
35
35
  */
36
+ import { delegatedResultMarker } from '@jini-ai/protocol';
36
37
  import { batchOutcomeFor, isReadOnlyTool } from '@jini-ai/core';
37
38
  import { ToolSchema } from '@modelcontextprotocol/sdk/types.js';
38
39
  import { randomUUID } from 'node:crypto';
@@ -154,9 +155,10 @@ function createDelegatedGateway(variant, { runId }, options) {
154
155
  },
155
156
  handler: async ({ args, ctx }) => {
156
157
  const toolId = requireString({ value: args.toolId, name: 'toolId' });
158
+ const toolUseId = generateToolUseId({});
157
159
  const body = {
158
160
  runId,
159
- toolUseId: generateToolUseId({}),
161
+ toolUseId,
160
162
  toolId: toolId,
161
163
  // The schema tells the model to omit `input` for a tool that takes none, but most tool
162
164
  // handlers refuse an absent input ("input must be an object"). Sending `{}` makes the
@@ -168,7 +170,11 @@ function createDelegatedGateway(variant, { runId }, options) {
168
170
  ...daemonCallOptions({ ctx }),
169
171
  timeoutMs,
170
172
  });
171
- return unwrapMcpContentEnvelope(data.result);
173
+ const output = unwrapMcpContentEnvelope(data.result);
174
+ // Only an unwrapped MCP envelope can correlate against the bridge's saved content digest.
175
+ if (!options.delegatedResultMarkers || !isRecord(output) || !Array.isArray(output.content))
176
+ return output;
177
+ return { ...output, content: [delegatedResultMarker({ toolUseId }), ...output.content] };
172
178
  },
173
179
  };
174
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/mcp",
3
- "version": "0.4.4",
3
+ "version": "0.6.10",
4
4
  "description": "Model Context Protocol adapters: server config schema and IO, an MCP client, and agent-side MCP server installation.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -83,11 +83,12 @@
83
83
  "registry": "https://registry.npmjs.org"
84
84
  },
85
85
  "dependencies": {
86
- "@jini-ai/core": "^0.4.9"
86
+ "@jini-ai/core": "^0.6.10",
87
+ "@jini-ai/protocol": "^0.6.10"
87
88
  },
88
89
  "peerDependencies": {
89
90
  "@cfworker/json-schema": "^4.1.1",
90
- "@jini-ai/platform": "^0.5.3",
91
+ "@jini-ai/platform": "^0.6.10",
91
92
  "@modelcontextprotocol/sdk": "^1.29.0"
92
93
  },
93
94
  "peerDependenciesMeta": {
@@ -104,9 +105,9 @@
104
105
  "devDependencies": {
105
106
  "@cfworker/json-schema": "^4.1.1",
106
107
  "@modelcontextprotocol/sdk": "^1.29.0",
107
- "@jini-ai/core": "^0.4.9",
108
- "@jini-ai/platform": "^0.5.10",
109
- "@jini-ai/daemon": "^0.6.9"
108
+ "@jini-ai/core": "^0.6.10",
109
+ "@jini-ai/daemon": "^0.6.10",
110
+ "@jini-ai/platform": "^0.6.10"
110
111
  },
111
112
  "scripts": {
112
113
  "build": "tsc -p tsconfig.json && chmod +x dist/bin/serve.js",