@jini-ai/mcp 0.3.3 → 0.4.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 +151 -0
- package/README.md +97 -22
- package/dist/agent-install/install.d.ts +20 -4
- package/dist/agent-install/install.js +11 -11
- package/dist/bin/serve.d.ts +25 -13
- package/dist/bin/serve.js +42 -21
- package/dist/client/client.d.ts +28 -8
- package/dist/client/client.js +25 -21
- package/dist/core/config.d.ts +57 -18
- package/dist/core/config.js +76 -48
- package/dist/core/index.d.ts +1 -2
- package/dist/core/index.js +1 -2
- package/dist/core/install-info.d.ts +4 -1
- package/dist/core/install-info.js +5 -5
- package/dist/core/tokens.d.ts +39 -13
- package/dist/core/tokens.js +54 -24
- package/dist/federation/adapter.http.d.ts +67 -0
- package/dist/federation/adapter.http.js +414 -0
- package/dist/federation/approvals/call-confirmation.d.ts +23 -0
- package/dist/federation/approvals/call-confirmation.js +124 -0
- package/dist/federation/approvals/index.d.ts +4 -0
- package/dist/federation/approvals/index.js +4 -0
- package/dist/federation/approvals/ports.d.ts +107 -0
- package/dist/federation/approvals/ports.js +2 -0
- package/dist/federation/approvals/revocation.d.ts +61 -0
- package/dist/federation/approvals/revocation.js +52 -0
- package/dist/federation/bootstrap.d.ts +100 -0
- package/dist/federation/bootstrap.js +182 -0
- package/dist/federation/config.d.ts +68 -0
- package/dist/federation/config.js +40 -0
- package/dist/federation/index.d.ts +14 -0
- package/dist/federation/index.js +14 -0
- package/dist/federation/mcp-protocol.d.ts +115 -0
- package/dist/federation/mcp-protocol.js +151 -0
- package/dist/federation/messages.d.ts +54 -0
- package/dist/federation/messages.js +35 -0
- package/dist/federation/ports.d.ts +287 -0
- package/dist/federation/ports.js +5 -0
- package/dist/federation/presets.d.ts +66 -0
- package/dist/federation/presets.js +30 -0
- package/dist/federation/refusal-notice.d.ts +198 -0
- package/dist/federation/refusal-notice.js +193 -0
- package/dist/federation/registrations.d.ts +171 -0
- package/dist/federation/registrations.js +181 -0
- package/dist/federation/reload.d.ts +112 -0
- package/dist/federation/reload.js +110 -0
- package/dist/federation/stdio/adapter.stdio.d.ts +157 -0
- package/dist/federation/stdio/adapter.stdio.js +526 -0
- package/dist/federation/stdio/default-connect.d.ts +29 -0
- package/dist/federation/stdio/default-connect.js +92 -0
- package/dist/federation/stdio/index.d.ts +4 -0
- package/dist/federation/stdio/index.js +4 -0
- package/dist/federation/stdio/stdio-launch-resolver.d.ts +139 -0
- package/dist/federation/stdio/stdio-launch-resolver.js +227 -0
- package/dist/federation/testing/adapter.memory.d.ts +177 -0
- package/dist/federation/testing/adapter.memory.js +189 -0
- package/dist/federation/testing/index.d.ts +3 -0
- package/dist/federation/testing/index.js +3 -0
- package/dist/federation/testing/tool-approvals.memory.d.ts +27 -0
- package/dist/federation/testing/tool-approvals.memory.js +38 -0
- package/dist/federation/tool-approvals.d.ts +97 -0
- package/dist/federation/tool-approvals.js +42 -0
- package/dist/federation/trust.d.ts +471 -0
- package/dist/federation/trust.js +707 -0
- package/dist/index.d.ts +175 -2
- package/dist/index.js +172 -2
- package/dist/server/daemon-client.d.ts +16 -4
- package/dist/server/daemon-client.js +41 -19
- package/dist/server/ports.d.ts +9 -0
- package/dist/server/ports.js +2 -0
- package/dist/server/resource-protocol.d.ts +14 -4
- package/dist/server/resource-protocol.js +6 -6
- package/dist/server/resources/active-resource.js +3 -3
- package/dist/server/sdk-transport.d.ts +11 -0
- package/dist/server/sdk-transport.js +31 -0
- package/dist/server/tool-protocol.d.ts +31 -9
- package/dist/server/tool-protocol.js +20 -18
- package/dist/server/tool-server.d.ts +60 -43
- package/dist/server/tool-server.js +51 -27
- package/dist/server/tools/component-catalog-tools.js +7 -7
- package/dist/server/tools/delegated-tool.d.ts +7 -4
- package/dist/server/tools/delegated-tool.js +14 -15
- package/dist/server/tools/run-tools.js +15 -15
- package/dist/server/tools/tool-catalog-tools.js +7 -7
- package/dist/tools/ask-choice/index.d.ts +123 -0
- package/dist/tools/ask-choice/index.js +98 -0
- package/dist/tools/ask-choice/pending.d.ts +11 -0
- package/dist/tools/ask-choice/pending.js +34 -0
- package/dist/tools/ask-choice/question.d.ts +16 -0
- package/dist/tools/ask-choice/question.js +60 -0
- package/dist/tools/ask-choice/schema.d.ts +91 -0
- package/dist/tools/ask-choice/schema.js +27 -0
- package/dist/tools/ask-choice/types.d.ts +106 -0
- package/dist/tools/ask-choice/types.js +2 -0
- package/package.json +43 -5
package/dist/bin/serve.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { isLoopbackHostname } from '@jini-ai/platform/net';
|
|
4
4
|
import { createMcpToolServer as createMcpToolServerDefault } from '../server/tool-server.js';
|
|
5
5
|
import { RUN_TOOLS } from '../server/tools/run-tools.js';
|
|
6
6
|
import { TOOL_CATALOG_TOOLS } from '../server/tools/tool-catalog-tools.js';
|
|
@@ -9,7 +9,7 @@ import { createExecuteDelegatedToolTool, createExecuteReadonlyDelegatedToolTool
|
|
|
9
9
|
import { KERNEL_RESOURCES } from '../server/resources/active-resource.js';
|
|
10
10
|
/** The one run this process is scoped to for its entire lifetime. Set by the spawning daemon, never by the MCP client. */
|
|
11
11
|
export const RUN_ID_ENV_VAR = 'JINI_RUN_ID';
|
|
12
|
-
/** Checked (only) when resolving the daemon HTTP base URL — see `resolveDaemonUrl`'s own docs; matches
|
|
12
|
+
/** Checked (only) when resolving the daemon HTTP base URL — see `resolveDaemonUrl`'s own docs; matches the CLI entry point's identically-named constant. */
|
|
13
13
|
export const DAEMON_URL_ENV_VAR = 'JINI_DAEMON_URL';
|
|
14
14
|
/**
|
|
15
15
|
* Bearer credential for this process's daemon callbacks, set by the spawning daemon when it issued
|
|
@@ -41,7 +41,32 @@ const SERVER_VERSION = '0.0.0';
|
|
|
41
41
|
const INSTRUCTIONS = 'Tools proxying this host\'s Jini daemon over loopback HTTP, scoped to the run that spawned this MCP server process. ' +
|
|
42
42
|
'Use execute_delegated_tool to invoke a Jini-registered tool (never an agent-vendor-specific tool name) against that run — ' +
|
|
43
43
|
'every call is routed through the daemon\'s deny-by-default ToolExecutor gate.';
|
|
44
|
-
|
|
44
|
+
// Keep the bin's environment-only assembly here; a general CLI resolver would introduce a
|
|
45
|
+
// runtime dependency on another shell. No discovery/default URL is selected for a run child.
|
|
46
|
+
async function resolveDaemonUrlDefault(_required, options) {
|
|
47
|
+
const url = options.env[options.envVarName];
|
|
48
|
+
if (!url)
|
|
49
|
+
throw new Error('no daemon URL resolved: pass flagUrl, set envVarName on a populated env var, ' +
|
|
50
|
+
'supply a discover() probe, or provide defaultUrl.');
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(url);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
if (parsed.protocol !== 'https:' && !isLoopbackHostname({ hostname: parsed.hostname })) {
|
|
59
|
+
let display = url;
|
|
60
|
+
if (parsed.username || parsed.password) {
|
|
61
|
+
parsed.username = '';
|
|
62
|
+
parsed.password = '';
|
|
63
|
+
display = parsed.toString();
|
|
64
|
+
}
|
|
65
|
+
options.warn({ message: `daemon URL ${display} is neither loopback nor HTTPS; traffic to a remote, non-HTTPS daemon is not encrypted.` });
|
|
66
|
+
}
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
function defaultWriteErr({ text }) {
|
|
45
70
|
process.stderr.write(text);
|
|
46
71
|
}
|
|
47
72
|
/**
|
|
@@ -52,25 +77,24 @@ function defaultWriteErr(text) {
|
|
|
52
77
|
* idle-exit window elapses (`createMcpToolServer`'s own default) — never bare-throws: every error
|
|
53
78
|
* path below writes a message to `writeErr` and calls `exit(1)` instead.
|
|
54
79
|
*/
|
|
55
|
-
export async function serve(deps = {}) {
|
|
80
|
+
export async function serve(_requiredArgs, deps = {}) {
|
|
56
81
|
const env = deps.env ?? process.env;
|
|
57
82
|
const writeErr = deps.writeErr ?? defaultWriteErr;
|
|
58
|
-
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
83
|
+
const exit = deps.exit ?? (({ code }) => process.exit(code));
|
|
59
84
|
const resolveDaemonUrlFn = deps.resolveDaemonUrl ?? resolveDaemonUrlDefault;
|
|
60
85
|
const createMcpToolServerFn = deps.createMcpToolServer ?? createMcpToolServerDefault;
|
|
61
86
|
const runId = env[RUN_ID_ENV_VAR];
|
|
62
87
|
if (typeof runId !== 'string' || runId.length === 0) {
|
|
63
|
-
writeErr(`${SERVER_NAME}: missing required ${RUN_ID_ENV_VAR} environment variable (this process must be spawned by a Jini daemon, which sets it)\n`);
|
|
64
|
-
return exit(1);
|
|
88
|
+
writeErr({ text: `${SERVER_NAME}: missing required ${RUN_ID_ENV_VAR} environment variable (this process must be spawned by a Jini daemon, which sets it)\n` });
|
|
89
|
+
return exit({ code: 1 });
|
|
65
90
|
}
|
|
66
91
|
const resolveBaseUrl = () => {
|
|
67
|
-
const options = { flagUrl: null, env, envVarName: DAEMON_URL_ENV_VAR, warn: writeErr };
|
|
68
|
-
return resolveDaemonUrlFn(options);
|
|
92
|
+
const options = { flagUrl: null, env, envVarName: DAEMON_URL_ENV_VAR, warn: ({ message }) => writeErr({ text: message }) };
|
|
93
|
+
return resolveDaemonUrlFn({}, options);
|
|
69
94
|
};
|
|
70
95
|
// Shared by both gateways so their run scope, correlation-id source, and deadline cannot diverge
|
|
71
96
|
// — the only difference between the two defs is the read-only constraint each declares.
|
|
72
97
|
const delegatedGatewayOptions = {
|
|
73
|
-
runId,
|
|
74
98
|
...(deps.generateToolUseId !== undefined ? { generateToolUseId: deps.generateToolUseId } : {}),
|
|
75
99
|
// Parsed, not validated: `createExecuteDelegatedToolTool` already rejects a non-finite or
|
|
76
100
|
// non-positive value back to the default, so `NaN` from a malformed env var lands there too.
|
|
@@ -82,11 +106,11 @@ export async function serve(deps = {}) {
|
|
|
82
106
|
...RUN_TOOLS,
|
|
83
107
|
...TOOL_CATALOG_TOOLS,
|
|
84
108
|
...COMPONENT_CATALOG_TOOLS,
|
|
85
|
-
createExecuteDelegatedToolTool(delegatedGatewayOptions),
|
|
109
|
+
createExecuteDelegatedToolTool({ runId }, delegatedGatewayOptions),
|
|
86
110
|
// Hosted ALONGSIDE the unconstrained gateway, never instead of it. A client that gates on
|
|
87
111
|
// `readOnlyHint: true` can now reach the read half of the catalog at all; every other client
|
|
88
112
|
// sees both and picks by what its call actually does.
|
|
89
|
-
createExecuteReadonlyDelegatedToolTool(delegatedGatewayOptions),
|
|
113
|
+
createExecuteReadonlyDelegatedToolTool({ runId }, delegatedGatewayOptions),
|
|
90
114
|
];
|
|
91
115
|
// Absent credential => no `authHeaders` at all => byte-identical request headers to before this
|
|
92
116
|
// env var existed. See DAEMON_TOKEN_ENV_VAR's doc for why an unset value is not a startup failure.
|
|
@@ -94,12 +118,9 @@ export async function serve(deps = {}) {
|
|
|
94
118
|
const authHeaders = typeof credential === 'string' && credential.length > 0
|
|
95
119
|
? { Authorization: `Bearer ${credential}` }
|
|
96
120
|
: undefined;
|
|
121
|
+
const requiredArgs = { name: SERVER_NAME, version: SERVER_VERSION, tools, resolveBaseUrl };
|
|
97
122
|
const serverOptions = {
|
|
98
|
-
name: SERVER_NAME,
|
|
99
|
-
version: SERVER_VERSION,
|
|
100
|
-
tools,
|
|
101
123
|
resources: KERNEL_RESOURCES,
|
|
102
|
-
resolveBaseUrl,
|
|
103
124
|
instructions: INSTRUCTIONS,
|
|
104
125
|
...(authHeaders !== undefined ? { authHeaders } : {}),
|
|
105
126
|
...(deps.fetchImpl !== undefined ? { fetchImpl: deps.fetchImpl } : {}),
|
|
@@ -107,19 +128,19 @@ export async function serve(deps = {}) {
|
|
|
107
128
|
...(deps.stdout !== undefined ? { stdout: deps.stdout } : {}),
|
|
108
129
|
};
|
|
109
130
|
try {
|
|
110
|
-
await createMcpToolServerFn(serverOptions).run();
|
|
131
|
+
await createMcpToolServerFn(requiredArgs, serverOptions).run({});
|
|
111
132
|
}
|
|
112
133
|
catch (err) {
|
|
113
134
|
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
-
writeErr(`${SERVER_NAME}: ${message}\n`);
|
|
115
|
-
return exit(1);
|
|
135
|
+
writeErr({ text: `${SERVER_NAME}: ${message}\n` });
|
|
136
|
+
return exit({ code: 1 });
|
|
116
137
|
}
|
|
117
138
|
}
|
|
118
139
|
// Only run for real when this module is the actual process entrypoint (`node dist/bin/serve.js`,
|
|
119
140
|
// or the `jini-mcp` bin shim pnpm/npm links to it) — not merely imported, e.g. by this file's own
|
|
120
|
-
// tests importing the named `serve` export above. Mirrors
|
|
141
|
+
// tests importing the named `serve` export above. Mirrors the CLI entry point's identical guard.
|
|
121
142
|
const isMainModule = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1];
|
|
122
143
|
if (isMainModule) {
|
|
123
|
-
await serve();
|
|
144
|
+
await serve({});
|
|
124
145
|
}
|
|
125
146
|
//# sourceMappingURL=serve.js.map
|
package/dist/client/client.d.ts
CHANGED
|
@@ -6,23 +6,37 @@
|
|
|
6
6
|
* textual-MIME classifier. Dependency-free (node stdlib only); imports no
|
|
7
7
|
* sibling MCP subdirectory.
|
|
8
8
|
*/
|
|
9
|
+
/** Scheduling boundary for hosts that manage their own timer lifecycle. */
|
|
10
|
+
export interface McpIdleTimerPort {
|
|
11
|
+
schedule(args: {
|
|
12
|
+
callback: () => void;
|
|
13
|
+
delayMs: number;
|
|
14
|
+
}): ReturnType<typeof setTimeout>;
|
|
15
|
+
cancel(args: {
|
|
16
|
+
handle: ReturnType<typeof setTimeout>;
|
|
17
|
+
}): void;
|
|
18
|
+
}
|
|
9
19
|
interface McpIdleExitControllerOptions {
|
|
10
20
|
idleMs: number;
|
|
11
|
-
onIdle: () => void;
|
|
21
|
+
onIdle: (required: Record<string, never>) => void;
|
|
12
22
|
}
|
|
13
23
|
/**
|
|
14
24
|
* Create an idle-exit controller that calls `onIdle` after `idleMs` of
|
|
15
|
-
* inactivity. Activity is tracked via `noteActivity()` and `trackRequest()`;
|
|
25
|
+
* inactivity. Activity is tracked via `noteActivity({})` and `trackRequest({ fn })`;
|
|
16
26
|
* in-flight requests defer the idle timer. Used to auto-close a long-running
|
|
17
27
|
* stdio MCP server process after a period of no tool calls.
|
|
18
28
|
* @param options Idle duration in ms (finite positive integer, clamped to a
|
|
19
29
|
* 24h ceiling — throws `RangeError` otherwise) and the callback to invoke on idle.
|
|
20
30
|
* @returns An object with `noteActivity`, `trackRequest`, and `dispose` methods.
|
|
21
31
|
*/
|
|
22
|
-
export declare function createMcpIdleExitController({ idleMs, onIdle, }: McpIdleExitControllerOptions
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
export declare function createMcpIdleExitController({ idleMs, onIdle, }: McpIdleExitControllerOptions, { timers }?: {
|
|
33
|
+
timers?: McpIdleTimerPort;
|
|
34
|
+
}): {
|
|
35
|
+
noteActivity(_required: Record<string, never>): void;
|
|
36
|
+
trackRequest<T>({ fn }: {
|
|
37
|
+
fn: (required: Record<string, never>) => T | Promise<T>;
|
|
38
|
+
}): Promise<T>;
|
|
39
|
+
dispose(_required: Record<string, never>): void;
|
|
26
40
|
};
|
|
27
41
|
/**
|
|
28
42
|
* True when `mime` names a textual content type whose body can be surfaced
|
|
@@ -31,7 +45,9 @@ export declare function createMcpIdleExitController({ idleMs, onIdle, }: McpIdle
|
|
|
31
45
|
* @param mime The MIME type string to classify (undefined counts as non-textual).
|
|
32
46
|
* @returns Whether the content should be treated as text.
|
|
33
47
|
*/
|
|
34
|
-
export declare function isTextualMime(mime
|
|
48
|
+
export declare function isTextualMime({ mime }: {
|
|
49
|
+
mime: string | undefined;
|
|
50
|
+
}): boolean;
|
|
35
51
|
/**
|
|
36
52
|
* Extract all relative (non-absolute, non-CDN, non-data-URI) file references from
|
|
37
53
|
* the text content of a file, normalized to root-relative paths.
|
|
@@ -43,6 +59,10 @@ export declare function isTextualMime(mime: string | undefined): boolean;
|
|
|
43
59
|
* @param fromMime The MIME type of the file (used to select reference-extraction patterns).
|
|
44
60
|
* @returns Deduplicated root-relative paths of all referenced files found.
|
|
45
61
|
*/
|
|
46
|
-
export declare function extractRelativeRefs(text
|
|
62
|
+
export declare function extractRelativeRefs({ text, fromPath, fromMime }: {
|
|
63
|
+
text: string;
|
|
64
|
+
fromPath: string;
|
|
65
|
+
fromMime: string;
|
|
66
|
+
}): string[];
|
|
47
67
|
export {};
|
|
48
68
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client/client.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
// references" bundling. They hold no state beyond the idle controller's own
|
|
12
12
|
// timer and never touch the network or filesystem, so they unit-test against
|
|
13
13
|
// plain values.
|
|
14
|
+
const defaultTimers = {
|
|
15
|
+
schedule: ({ callback, delayMs }) => setTimeout(callback, delayMs),
|
|
16
|
+
cancel: ({ handle }) => clearTimeout(handle),
|
|
17
|
+
};
|
|
14
18
|
// SEC-RB-011: an unvalidated `idleMs` (negative, `NaN`, `Infinity`, or a
|
|
15
19
|
// non-integer) would either schedule an immediately/never-firing timer or
|
|
16
20
|
// hand `setTimeout` a value it silently coerces in surprising ways. Bound it
|
|
@@ -28,21 +32,21 @@ function normalizeIdleMs(idleMs) {
|
|
|
28
32
|
}
|
|
29
33
|
/**
|
|
30
34
|
* Create an idle-exit controller that calls `onIdle` after `idleMs` of
|
|
31
|
-
* inactivity. Activity is tracked via `noteActivity()` and `trackRequest()`;
|
|
35
|
+
* inactivity. Activity is tracked via `noteActivity({})` and `trackRequest({ fn })`;
|
|
32
36
|
* in-flight requests defer the idle timer. Used to auto-close a long-running
|
|
33
37
|
* stdio MCP server process after a period of no tool calls.
|
|
34
38
|
* @param options Idle duration in ms (finite positive integer, clamped to a
|
|
35
39
|
* 24h ceiling — throws `RangeError` otherwise) and the callback to invoke on idle.
|
|
36
40
|
* @returns An object with `noteActivity`, `trackRequest`, and `dispose` methods.
|
|
37
41
|
*/
|
|
38
|
-
export function createMcpIdleExitController({ idleMs, onIdle, }) {
|
|
42
|
+
export function createMcpIdleExitController({ idleMs, onIdle, }, { timers = defaultTimers } = {}) {
|
|
39
43
|
const effectiveIdleMs = normalizeIdleMs(idleMs);
|
|
40
44
|
let timer = null;
|
|
41
45
|
let inFlight = 0;
|
|
42
46
|
let disposed = false;
|
|
43
47
|
const clear = () => {
|
|
44
48
|
if (timer) {
|
|
45
|
-
|
|
49
|
+
timers.cancel({ handle: timer });
|
|
46
50
|
timer = null;
|
|
47
51
|
}
|
|
48
52
|
};
|
|
@@ -50,26 +54,26 @@ export function createMcpIdleExitController({ idleMs, onIdle, }) {
|
|
|
50
54
|
if (disposed)
|
|
51
55
|
return;
|
|
52
56
|
clear();
|
|
53
|
-
timer =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
timer = timers.schedule({ callback: () => {
|
|
58
|
+
// `dispose({})` always clears the pending timer and `schedule()` bails when
|
|
59
|
+
// already disposed, so this callback can only fire while live.
|
|
60
|
+
timer = null;
|
|
61
|
+
if (inFlight > 0) {
|
|
62
|
+
schedule();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
disposed = true;
|
|
66
|
+
onIdle({});
|
|
67
|
+
}, delayMs: effectiveIdleMs });
|
|
64
68
|
};
|
|
65
69
|
schedule();
|
|
66
70
|
return {
|
|
67
|
-
noteActivity() {
|
|
71
|
+
noteActivity(_required) {
|
|
68
72
|
schedule();
|
|
69
73
|
},
|
|
70
|
-
async trackRequest(fn) {
|
|
74
|
+
async trackRequest({ fn }) {
|
|
71
75
|
if (disposed) {
|
|
72
|
-
return fn();
|
|
76
|
+
return fn({});
|
|
73
77
|
}
|
|
74
78
|
inFlight += 1;
|
|
75
79
|
// Deliberately does NOT call `schedule()` here (SEC-RB-011): a request
|
|
@@ -80,7 +84,7 @@ export function createMcpIdleExitController({ idleMs, onIdle, }) {
|
|
|
80
84
|
// on every concurrent request start instead caused needless
|
|
81
85
|
// clearTimeout/setTimeout churn under load with no behavioral benefit.
|
|
82
86
|
try {
|
|
83
|
-
return await fn();
|
|
87
|
+
return await fn({});
|
|
84
88
|
}
|
|
85
89
|
finally {
|
|
86
90
|
inFlight -= 1;
|
|
@@ -89,7 +93,7 @@ export function createMcpIdleExitController({ idleMs, onIdle, }) {
|
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
},
|
|
92
|
-
dispose() {
|
|
96
|
+
dispose(_required) {
|
|
93
97
|
disposed = true;
|
|
94
98
|
clear();
|
|
95
99
|
},
|
|
@@ -114,7 +118,7 @@ const TEXTUAL_MIME_PATTERNS = [
|
|
|
114
118
|
* @param mime The MIME type string to classify (undefined counts as non-textual).
|
|
115
119
|
* @returns Whether the content should be treated as text.
|
|
116
120
|
*/
|
|
117
|
-
export function isTextualMime(mime) {
|
|
121
|
+
export function isTextualMime({ mime }) {
|
|
118
122
|
if (!mime)
|
|
119
123
|
return false;
|
|
120
124
|
return TEXTUAL_MIME_PATTERNS.some((re) => re.test(mime));
|
|
@@ -169,7 +173,7 @@ function isHtmlLike(mime, fromPath) {
|
|
|
169
173
|
* @param fromMime The MIME type of the file (used to select reference-extraction patterns).
|
|
170
174
|
* @returns Deduplicated root-relative paths of all referenced files found.
|
|
171
175
|
*/
|
|
172
|
-
export function extractRelativeRefs(text, fromPath, fromMime) {
|
|
176
|
+
export function extractRelativeRefs({ text, fromPath, fromMime }) {
|
|
173
177
|
if (!text)
|
|
174
178
|
return [];
|
|
175
179
|
const refs = new Set();
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module @jini-ai/mcp/core/config
|
|
3
|
-
* Config schema + on-disk store for the external MCP servers the daemon connects
|
|
4
|
-
* to as a client, plus the per-agent config builders (Claude `.mcp.json`, ACP
|
|
5
|
-
* `mcpServers`, OpenCode) that hand those servers to a launching agent. Part of
|
|
6
|
-
* the MCP `core` kernel; depends on no sibling subdirectory.
|
|
7
|
-
*/
|
|
8
1
|
/** Wire-level transport discriminator for how the daemon connects to an external MCP server. */
|
|
9
2
|
export type McpTransport = 'stdio' | 'sse' | 'http';
|
|
10
3
|
/** Authentication mode for remote MCP servers: `'none'` for local/loopback targets, `'oauth'` for remote ones. */
|
|
@@ -29,6 +22,16 @@ export interface McpServerConfig {
|
|
|
29
22
|
export interface McpConfig {
|
|
30
23
|
servers: McpServerConfig[];
|
|
31
24
|
}
|
|
25
|
+
/** Text I/O boundary; secret writes must be atomic and owner-only from creation. */
|
|
26
|
+
export interface McpConfigFilesystemPort {
|
|
27
|
+
readText(args: {
|
|
28
|
+
filePath: string;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
writeSecretText(args: {
|
|
31
|
+
filePath: string;
|
|
32
|
+
contents: string;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
}
|
|
32
35
|
/**
|
|
33
36
|
* Infer the appropriate `McpAuthMode` for a given server URL.
|
|
34
37
|
* Returns `'none'` for loopback targets (localhost, 127.x.x.x, ::1)
|
|
@@ -36,13 +39,17 @@ export interface McpConfig {
|
|
|
36
39
|
* @param rawUrl The server URL string to inspect, if any.
|
|
37
40
|
* @returns The inferred `McpAuthMode`.
|
|
38
41
|
*/
|
|
39
|
-
export declare function inferMcpAuthModeForUrl(rawUrl
|
|
42
|
+
export declare function inferMcpAuthModeForUrl({ rawUrl }: {
|
|
43
|
+
rawUrl: string | undefined;
|
|
44
|
+
}): McpAuthMode;
|
|
40
45
|
/**
|
|
41
46
|
* Validate a single user-supplied entry. Drops invalid fields so a typo in
|
|
42
47
|
* one server doesn't tank the whole config. Returns null when the entry is
|
|
43
48
|
* unsalvageable (no id, or no transport-required fields).
|
|
44
49
|
*/
|
|
45
|
-
export declare function sanitizeMcpServer(raw
|
|
50
|
+
export declare function sanitizeMcpServer({ raw }: {
|
|
51
|
+
raw: unknown;
|
|
52
|
+
}): McpServerConfig | null;
|
|
46
53
|
/**
|
|
47
54
|
* Coerce and de-duplicate a freeform JSON blob into a valid `McpConfig`.
|
|
48
55
|
* Entries that fail `sanitizeMcpServer` are silently dropped; duplicate ids
|
|
@@ -50,14 +57,20 @@ export declare function sanitizeMcpServer(raw: unknown): McpServerConfig | null;
|
|
|
50
57
|
* @param raw The raw parsed JSON to sanitize.
|
|
51
58
|
* @returns A clean `McpConfig` guaranteed to have only valid, unique server entries.
|
|
52
59
|
*/
|
|
53
|
-
export declare function sanitizeMcpConfig(raw
|
|
60
|
+
export declare function sanitizeMcpConfig({ raw }: {
|
|
61
|
+
raw: unknown;
|
|
62
|
+
}): McpConfig;
|
|
54
63
|
/**
|
|
55
64
|
* Read and sanitize the MCP config from `<dataDir>/mcp-config.json`.
|
|
56
65
|
* Returns `{ servers: [] }` when the file does not exist or contains invalid JSON.
|
|
57
66
|
* @param dataDir The resolved runtime data directory.
|
|
58
67
|
* @returns The sanitized `McpConfig`, never rejects on missing-file or corrupt-JSON.
|
|
59
68
|
*/
|
|
60
|
-
export declare function readMcpConfig(dataDir
|
|
69
|
+
export declare function readMcpConfig({ dataDir }: {
|
|
70
|
+
dataDir: string;
|
|
71
|
+
}, { filesystem }?: {
|
|
72
|
+
filesystem?: McpConfigFilesystemPort;
|
|
73
|
+
}): Promise<McpConfig>;
|
|
61
74
|
/**
|
|
62
75
|
* Sanitize and atomically write the MCP config to `<dataDir>/mcp-config.json`.
|
|
63
76
|
* Uses a per-`dataDir` promise-chain mutex so concurrent writes serialize
|
|
@@ -65,13 +78,18 @@ export declare function readMcpConfig(dataDir: string): Promise<McpConfig>;
|
|
|
65
78
|
* being written. This file may carry environment variables and
|
|
66
79
|
* `Authorization` header values (API keys, bearer tokens — see
|
|
67
80
|
* `McpServerConfig.env`/`.headers`), so it is written with owner-only
|
|
68
|
-
* (0600) permissions from the very first byte via `
|
|
81
|
+
* (0600) permissions from the very first byte via `writeFileAtomicAsync`
|
|
69
82
|
* rather than a post-rename chmod (CR-006 / SEC-RB-002).
|
|
70
83
|
* @param dataDir The resolved runtime data directory.
|
|
71
84
|
* @param body The raw config body to sanitize and persist.
|
|
72
85
|
* @returns The sanitized `McpConfig` that was written to disk.
|
|
73
86
|
*/
|
|
74
|
-
export declare function writeMcpConfig(dataDir
|
|
87
|
+
export declare function writeMcpConfig({ dataDir, body }: {
|
|
88
|
+
dataDir: string;
|
|
89
|
+
body: unknown;
|
|
90
|
+
}, { filesystem }?: {
|
|
91
|
+
filesystem?: McpConfigFilesystemPort;
|
|
92
|
+
}): Promise<McpConfig>;
|
|
75
93
|
/**
|
|
76
94
|
* True when `cwd` is a daemon-managed project directory under `projectsDir`
|
|
77
95
|
* (= safe to write `.mcp.json` into without risk of clobbering a user-owned
|
|
@@ -82,11 +100,18 @@ export declare function writeMcpConfig(dataDir: string, body: unknown): Promise<
|
|
|
82
100
|
* Both `cwd` and `projectsDir` are realpath-resolved before the containment
|
|
83
101
|
* check (falling back to lexical `path.resolve` normalization for a path
|
|
84
102
|
* that doesn't exist on disk yet), and containment is decided with
|
|
85
|
-
* `@jini-ai/
|
|
103
|
+
* `@jini-ai/core/primitives`' separator-aware `pathContains` rather than a raw
|
|
86
104
|
* string-prefix check — a `..`-containing path or a symlinked descendant
|
|
87
105
|
* can defeat a plain `startsWith` (CR-006 / SEC-RB-011).
|
|
88
106
|
*/
|
|
89
|
-
export declare function isManagedProjectCwd(cwd
|
|
107
|
+
export declare function isManagedProjectCwd({ cwd, projectsDir }: {
|
|
108
|
+
cwd: string | null | undefined;
|
|
109
|
+
projectsDir: string;
|
|
110
|
+
}, { resolvePath }?: {
|
|
111
|
+
resolvePath?: (args: {
|
|
112
|
+
filePath: string;
|
|
113
|
+
}) => string;
|
|
114
|
+
}): boolean;
|
|
90
115
|
/**
|
|
91
116
|
* Project-cwd `.mcp.json` shape that Claude Code auto-loads on spawn (the
|
|
92
117
|
* same format Claude Desktop and Cursor use). Returns null when the user
|
|
@@ -101,7 +126,11 @@ export declare function isManagedProjectCwd(cwd: string | null | undefined, proj
|
|
|
101
126
|
* pre-authenticated credentials. User-supplied headers always win on
|
|
102
127
|
* conflict so they can pin a specific token if they really want to.
|
|
103
128
|
*/
|
|
104
|
-
export declare function buildClaudeMcpJson(servers
|
|
129
|
+
export declare function buildClaudeMcpJson({ servers }: {
|
|
130
|
+
servers: McpServerConfig[];
|
|
131
|
+
}, { tokens }?: {
|
|
132
|
+
tokens?: Record<string, string>;
|
|
133
|
+
}): unknown | null;
|
|
105
134
|
/**
|
|
106
135
|
* Convert user-configured external MCP servers into the ACP `mcpServers`
|
|
107
136
|
* shape that stdio-capable ACP agents accept.
|
|
@@ -124,7 +153,9 @@ export interface AcpMcpServer {
|
|
|
124
153
|
* @param servers The full server list from `McpConfig`.
|
|
125
154
|
* @returns ACP-shaped server entries for all enabled stdio servers.
|
|
126
155
|
*/
|
|
127
|
-
export declare function buildAcpMcpServers(servers
|
|
156
|
+
export declare function buildAcpMcpServers({ servers }: {
|
|
157
|
+
servers: McpServerConfig[];
|
|
158
|
+
}): AcpMcpServer[];
|
|
128
159
|
/**
|
|
129
160
|
* OpenCode merges its config from multiple sources at boot — global
|
|
130
161
|
* `~/.config/opencode/opencode.json`, the `OPENCODE_CONFIG` file path, the
|
|
@@ -148,10 +179,18 @@ export declare function buildAcpMcpServers(servers: McpServerConfig[]): AcpMcpSe
|
|
|
148
179
|
*/
|
|
149
180
|
/** Options for `buildOpenCodeMcpConfigContent`. */
|
|
150
181
|
export interface OpenCodeConfigBuildOptions {
|
|
182
|
+
/** Access tokens keyed by server id. */
|
|
183
|
+
tokens?: Record<string, string>;
|
|
184
|
+
/** Resolves symlinks for permission allowlists. */
|
|
185
|
+
resolvePath?: (args: {
|
|
186
|
+
filePath: string;
|
|
187
|
+
}) => string;
|
|
151
188
|
/** Absolute filesystem paths OpenCode's `external_directory` permission should allow. */
|
|
152
189
|
allowedDirectories?: string[];
|
|
153
190
|
/** Additional top-level keys merged into the emitted config object (e.g. `provider`). */
|
|
154
191
|
extraConfig?: Record<string, unknown>;
|
|
155
192
|
}
|
|
156
|
-
export declare function buildOpenCodeMcpConfigContent(servers
|
|
193
|
+
export declare function buildOpenCodeMcpConfigContent({ servers }: {
|
|
194
|
+
servers: McpServerConfig[];
|
|
195
|
+
}, options?: OpenCodeConfigBuildOptions): string | null;
|
|
157
196
|
//# sourceMappingURL=config.d.ts.map
|