@corenel/mcp 0.1.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/LICENSE +93 -0
- package/dist/asAgentTools.d.ts +12 -0
- package/dist/asAgentTools.d.ts.map +1 -0
- package/dist/asAgentTools.js +71 -0
- package/dist/asAgentTools.js.map +1 -0
- package/dist/auth.d.ts +25 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +97 -0
- package/dist/auth.js.map +1 -0
- package/dist/cache.d.ts +17 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +81 -0
- package/dist/cache.js.map +1 -0
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +239 -0
- package/dist/config.js.map +1 -0
- package/dist/gatedAgentTools.d.ts +12 -0
- package/dist/gatedAgentTools.d.ts.map +1 -0
- package/dist/gatedAgentTools.js +36 -0
- package/dist/gatedAgentTools.js.map +1 -0
- package/dist/httpClient.d.ts +47 -0
- package/dist/httpClient.d.ts.map +1 -0
- package/dist/httpClient.js +196 -0
- package/dist/httpClient.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/discovery.d.ts +55 -0
- package/dist/oauth/discovery.d.ts.map +1 -0
- package/dist/oauth/discovery.js +211 -0
- package/dist/oauth/discovery.js.map +1 -0
- package/dist/oauth/login.d.ts +41 -0
- package/dist/oauth/login.d.ts.map +1 -0
- package/dist/oauth/login.js +206 -0
- package/dist/oauth/login.js.map +1 -0
- package/dist/oauth/loopback.d.ts +58 -0
- package/dist/oauth/loopback.d.ts.map +1 -0
- package/dist/oauth/loopback.js +165 -0
- package/dist/oauth/loopback.js.map +1 -0
- package/dist/oauth/pkce.d.ts +45 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +67 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/register.d.ts +34 -0
- package/dist/oauth/register.d.ts.map +1 -0
- package/dist/oauth/register.js +57 -0
- package/dist/oauth/register.js.map +1 -0
- package/dist/oauth/tokens.d.ts +62 -0
- package/dist/oauth/tokens.d.ts.map +1 -0
- package/dist/oauth/tokens.js +118 -0
- package/dist/oauth/tokens.js.map +1 -0
- package/dist/policy.d.ts +10 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +71 -0
- package/dist/policy.js.map +1 -0
- package/dist/registry.d.ts +124 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +265 -0
- package/dist/registry.js.map +1 -0
- package/dist/timedFetch.d.ts +10 -0
- package/dist/timedFetch.d.ts.map +1 -0
- package/dist/timedFetch.js +39 -0
- package/dist/timedFetch.js.map +1 -0
- package/dist/toolNames.d.ts +2 -0
- package/dist/toolNames.d.ts.map +1 -0
- package/dist/toolNames.js +14 -0
- package/dist/toolNames.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
package/dist/policy.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* The MCP policy gate — the single check nothing under `mcp:` may run
|
|
2
|
+
* without. Task 10 (daemon wiring) and Task 11 (socket messages) both sit
|
|
3
|
+
* downstream of this; this file owns the actual security decision.
|
|
4
|
+
*
|
|
5
|
+
* Deny by default, and NAME THE TOOL not the server:
|
|
6
|
+
*
|
|
7
|
+
* An MCP server describes its own tool list at runtime — there is no static
|
|
8
|
+
* catalog an operator can review the way there is for `fs:*`/`shell:*`
|
|
9
|
+
* before writing a policy. The ordinary guardrail's `tools.allow` treats an
|
|
10
|
+
* EMPTY list as "no restriction" (see `evaluateToolCall` in
|
|
11
|
+
* `packages/harness/guardrail/guardrail.ts`: `if (allow.length && ...)`).
|
|
12
|
+
* That default is wrong here, so this module never reuses that code path —
|
|
13
|
+
* an `mcp:` address with no matching entry is refused, full stop.
|
|
14
|
+
*
|
|
15
|
+
* The allow-list entry must match the exact `mcp:<server>/<tool>` address.
|
|
16
|
+
* Reusing the existing glob matcher (`matchesAnyGlob`) gets "names the tool,
|
|
17
|
+
* not the server" for free, with no special case: `mcp:teams` as a glob
|
|
18
|
+
* pattern has no `*` in it, so it matches only the literal string
|
|
19
|
+
* "mcp:teams" — never "mcp:teams/post_message". A server that starts with
|
|
20
|
+
* only read tools and later grows a destructive one must not inherit trust
|
|
21
|
+
* granted before that tool existed; an operator who really means "trust
|
|
22
|
+
* every tool this server ever exposes" has to write `mcp:teams/*` and mean
|
|
23
|
+
* it.
|
|
24
|
+
*
|
|
25
|
+
* Why every MCP tool is treated as mutating: `readOnlyHint` is an optional
|
|
26
|
+
* hint volunteered BY the server being evaluated for trust, so "read a page"
|
|
27
|
+
* and "post a message" arrive indistinguishable. This is the same rule the
|
|
28
|
+
* ACP work already established (an unknown tool kind MUST mean mutating, or
|
|
29
|
+
* unlabelled destructive calls auto-approve) — restated here rather than
|
|
30
|
+
* trusting any annotation the server itself supplies.
|
|
31
|
+
*
|
|
32
|
+
* Never prompts: an `ask` permission level normally means the ordinary
|
|
33
|
+
* `PermissionService.check` calls out to a human via a `PermissionPrompt`.
|
|
34
|
+
* The motivating case for MCP tools is an agent finishing an unattended job
|
|
35
|
+
* and messaging out about it — nobody is there to answer, so `ask` would
|
|
36
|
+
* hang the run rather than protect it. This function is synchronous and
|
|
37
|
+
* never holds a `PermissionPrompt`, but that alone isn't enough: Task 10's
|
|
38
|
+
* wiring could still name a tool in `tools.allow` and then separately run it
|
|
39
|
+
* through `PermissionService.check`, which WOULD prompt if the resolved
|
|
40
|
+
* level is "ask" and a prompt handler is wired up. So this gate resolves the
|
|
41
|
+
* permission level itself and refuses outright on anything but "allow" —
|
|
42
|
+
* `ask` and `deny` both come back as a refusal, never as an invitation to
|
|
43
|
+
* ask elsewhere. `defaultForReadOnly`/`defaultForMutating` are overridden to
|
|
44
|
+
* "allow" so an address with NO explicit entry in `permissions` /
|
|
45
|
+
* `mutatingDefault` / `defaultPermission` isn't itself vetoed by the
|
|
46
|
+
* ordinary "mutating tools ask by default" heuristic — being named in
|
|
47
|
+
* `tools.allow` is what authorises it; only an EXPLICIT ask/deny rule left
|
|
48
|
+
* in the policy can still veto it here.
|
|
49
|
+
*/
|
|
50
|
+
import { PermissionService, matchesAnyGlob } from '@corenel/harness/guardrail';
|
|
51
|
+
/** Deny-by-default gate for one `mcp:<server>/<tool>` address. */
|
|
52
|
+
export function mcpCallAllowed(address, policy) {
|
|
53
|
+
const allow = policy.tools?.allow ?? [];
|
|
54
|
+
if (!matchesAnyGlob(address, allow)) {
|
|
55
|
+
return { ok: false, reason: `${address} is not allowed - add it with: corenel config add mcpAllow ${address}` };
|
|
56
|
+
}
|
|
57
|
+
const service = new PermissionService({
|
|
58
|
+
policy,
|
|
59
|
+
defaultForReadOnly: 'allow',
|
|
60
|
+
defaultForMutating: 'allow',
|
|
61
|
+
});
|
|
62
|
+
const level = service.resolveLevel({ name: address, address, mutates: true });
|
|
63
|
+
if (level !== 'allow') {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: `${address} resolves to permission "${level}" - MCP tools never prompt; set it to "allow" or remove the rule`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { ok: true };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAK/E,kEAAkE;AAClE,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,MAAc;IAC5D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC;IACxC,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,8DAA8D,OAAO,EAAE,EAAE,CAAC;IAClH,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC;QACpC,MAAM;QACN,kBAAkB,EAAE,OAAO;QAC3B,kBAAkB,EAAE,OAAO;KAC5B,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,OAAO,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9E,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QACtB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG,OAAO,4BAA4B,KAAK,kEAAkE;SACtH,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { McpServer } from './types';
|
|
2
|
+
import type { McpTool } from './httpClient';
|
|
3
|
+
import type { CachedServer } from './cache';
|
|
4
|
+
/** The subset of `McpHttpClient` the registry needs. Named separately (not
|
|
5
|
+
* just `McpHttpClient` itself) so a test double can implement it without
|
|
6
|
+
* standing up the real class or its constructor's auth/fetch plumbing. */
|
|
7
|
+
export interface McpClient {
|
|
8
|
+
initialize(): Promise<void>;
|
|
9
|
+
listTools(): Promise<McpTool[]>;
|
|
10
|
+
callTool(name: string, args: unknown): Promise<{
|
|
11
|
+
content: unknown;
|
|
12
|
+
isError: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
/** Builds the client to use for one server. Defaults to the real
|
|
16
|
+
* `McpHttpClient` wired to the real `authProviderFor`; a test supplies a
|
|
17
|
+
* factory that returns a fake and never touches the network or the auth
|
|
18
|
+
* store. */
|
|
19
|
+
export type McpClientFactory = (stateDir: string, server: McpServer) => McpClient;
|
|
20
|
+
/**
|
|
21
|
+
* The daemon's OWN Corenel account token, or null when it has none.
|
|
22
|
+
*
|
|
23
|
+
* Exported because it is the part with behaviour: a relayed server is refused
|
|
24
|
+
* by api.corenel.ai's own auth without it, and the daemon reports that as
|
|
25
|
+
* `refused the credential (401)` -- which reads as a Microsoft problem and is
|
|
26
|
+
* not one.
|
|
27
|
+
*
|
|
28
|
+
* Null on ANY failure, deliberately. Signed out, no credential file, an
|
|
29
|
+
* unreadable one: all mean the same thing to the caller, and authProviderFor
|
|
30
|
+
* turns null into "send no Authorization header" so the relay answers its own
|
|
31
|
+
* clear "not authenticated" rather than something about Microsoft.
|
|
32
|
+
*/
|
|
33
|
+
export declare function accountTokenFor(stateDir: string): Promise<string | null>;
|
|
34
|
+
/** One server's outcome, connected or not, for `status()` to report. Kept
|
|
35
|
+
* separately from `Connected` (rather than folding an optional client onto
|
|
36
|
+
* one record) so a failed server carries exactly the fields it can: no
|
|
37
|
+
* tools, no client, an error naming why. */
|
|
38
|
+
interface ServerStatus {
|
|
39
|
+
connected: boolean;
|
|
40
|
+
toolCount: number;
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
/** The longest one server may take to connect -- credential refresh,
|
|
44
|
+
* initialize and tools/list together -- before it is reported as timed out
|
|
45
|
+
* and the daemon moves on without it. Generous for a cold service; short
|
|
46
|
+
* enough that a stalled one reads as a failure, not a hang. */
|
|
47
|
+
export declare const SERVER_CONNECT_TIMEOUT_MS = 30000;
|
|
48
|
+
/** One server's outcome, as reported the moment it settles. */
|
|
49
|
+
export type ServerReport = {
|
|
50
|
+
server: string;
|
|
51
|
+
} & ServerStatus;
|
|
52
|
+
export interface ConnectAllOptions {
|
|
53
|
+
/** Per-server connect deadline; defaults to SERVER_CONNECT_TIMEOUT_MS. */
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
/** Called once per server as soon as it settles -- connected, failed or
|
|
56
|
+
* timed out -- so a caller can report progress instead of going silent
|
|
57
|
+
* until the slowest server finishes. */
|
|
58
|
+
onServer?: (report: ServerReport) => void;
|
|
59
|
+
}
|
|
60
|
+
export declare class McpRegistry {
|
|
61
|
+
private readonly stateDir;
|
|
62
|
+
private readonly clientFactory;
|
|
63
|
+
private readonly connected;
|
|
64
|
+
private readonly statuses;
|
|
65
|
+
private serverOrder;
|
|
66
|
+
private readonly primed;
|
|
67
|
+
private readonly connecting;
|
|
68
|
+
constructor(stateDir: string, clientFactory?: McpClientFactory);
|
|
69
|
+
/**
|
|
70
|
+
* Connect to every configured server and cache its tools. Never throws --
|
|
71
|
+
* a server that fails to initialize, or fails listing tools, is recorded
|
|
72
|
+
* in `status()` with zero tools and the daemon keeps starting.
|
|
73
|
+
*/
|
|
74
|
+
connectAll(opts?: ConnectAllOptions): Promise<void>;
|
|
75
|
+
private connectOne;
|
|
76
|
+
/** Every tool from every connected OR primed server, paired with the server
|
|
77
|
+
* it came from, in CONFIGURED order (`serverOrder`) -- not the order
|
|
78
|
+
* servers happened to finish connecting in, and not insertion order into
|
|
79
|
+
* `primed` either. Within a server, tools stay in the order that server
|
|
80
|
+
* returned them. A live connection wins over a primed (cached) entry for
|
|
81
|
+
* the same server, since it is the fresher truth. A server with neither
|
|
82
|
+
* either failed to connect or reported zero tools -- `status()` tells the
|
|
83
|
+
* two apart. */
|
|
84
|
+
tools(): {
|
|
85
|
+
server: string;
|
|
86
|
+
tool: McpTool;
|
|
87
|
+
}[];
|
|
88
|
+
/** Seed tool lists from the on-disk cache. NO I/O, no sockets -- this is
|
|
89
|
+
* what a terminal calls at startup so that `tools()` can answer
|
|
90
|
+
* immediately, before any server has actually been reached. A primed
|
|
91
|
+
* server is deliberately NOT `connected`: it can answer what tools exist,
|
|
92
|
+
* and cannot call one until `ensureConnected()` has run. */
|
|
93
|
+
primeFromCache(cache: Record<string, CachedServer>): void;
|
|
94
|
+
isConnected(server: string): boolean;
|
|
95
|
+
connectedServers(): string[];
|
|
96
|
+
/** Connect ONE server, on demand. Never throws, for the same reason
|
|
97
|
+
* connectAll never throws: a server that is down must not take the
|
|
98
|
+
* process with it. Concurrent callers for the same server share one
|
|
99
|
+
* attempt rather than opening a second socket. */
|
|
100
|
+
ensureConnected(server: string): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Call one tool on one server. Fails immediately, naming the server, when
|
|
103
|
+
* that server never connected -- there is nothing to route the call
|
|
104
|
+
* through, and pretending otherwise would either stall waiting on a
|
|
105
|
+
* connection that will never appear or return an empty result the caller
|
|
106
|
+
* could mistake for a genuine (if empty) tool response.
|
|
107
|
+
*/
|
|
108
|
+
call(server: string, tool: string, args: unknown): Promise<{
|
|
109
|
+
content: unknown;
|
|
110
|
+
isError: boolean;
|
|
111
|
+
}>;
|
|
112
|
+
/** Per-server connection state, for `doctor` (Task 10) to print. A server
|
|
113
|
+
* with `connected: false` always carries `error` -- there is no code path
|
|
114
|
+
* that records a failure without a reason, because "no tools" alone tells
|
|
115
|
+
* the reader nothing about what to fix. */
|
|
116
|
+
status(): {
|
|
117
|
+
server: string;
|
|
118
|
+
connected: boolean;
|
|
119
|
+
toolCount: number;
|
|
120
|
+
error?: string;
|
|
121
|
+
}[];
|
|
122
|
+
}
|
|
123
|
+
export {};
|
|
124
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAM5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;2EAE2E;AAC3E,MAAM,WAAW,SAAS;IACxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,SAAS,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACxF;AAED;;;aAGa;AACb,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,SAAS,CAAC;AAElF;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO9E;AAmCD;;;6CAG6C;AAC7C,UAAU,YAAY;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;gEAGgE;AAChE,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;6CAEyC;IACzC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;CAC3C;AAED,qBAAa,WAAW;IA0BpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IA1BhC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAC1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAW5D,OAAO,CAAC,WAAW,CAAgB;IAMnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IAIvD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoC;gBAG5C,QAAQ,EAAE,MAAM,EAChB,aAAa,GAAE,gBAAuC;IAGzE;;;;OAIG;IACG,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;YAiC/C,UAAU;IAyCxB;;;;;;;qBAOiB;IACjB,KAAK,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EAAE;IAW5C;;;;iEAI6D;IAC7D,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,IAAI;IAQzD,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIpC,gBAAgB,IAAI,MAAM,EAAE;IAI5B;;;uDAGmD;IAC7C,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBpD;;;;;;OAMG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAYxG;;;gDAG4C;IAC5C,MAAM,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE;CAYtF"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { McpHttpClient } from './httpClient.js';
|
|
2
|
+
import { authProviderFor } from './auth.js';
|
|
3
|
+
import { resolveCredential } from '@corenel/config';
|
|
4
|
+
import { readMcpServers } from './config.js';
|
|
5
|
+
import { timedFetch, AUTH_REQUEST_TIMEOUT_MS } from './timedFetch.js';
|
|
6
|
+
/**
|
|
7
|
+
* The daemon's OWN Corenel account token, or null when it has none.
|
|
8
|
+
*
|
|
9
|
+
* Exported because it is the part with behaviour: a relayed server is refused
|
|
10
|
+
* by api.corenel.ai's own auth without it, and the daemon reports that as
|
|
11
|
+
* `refused the credential (401)` -- which reads as a Microsoft problem and is
|
|
12
|
+
* not one.
|
|
13
|
+
*
|
|
14
|
+
* Null on ANY failure, deliberately. Signed out, no credential file, an
|
|
15
|
+
* unreadable one: all mean the same thing to the caller, and authProviderFor
|
|
16
|
+
* turns null into "send no Authorization header" so the relay answers its own
|
|
17
|
+
* clear "not authenticated" rather than something about Microsoft.
|
|
18
|
+
*/
|
|
19
|
+
export async function accountTokenFor(stateDir) {
|
|
20
|
+
try {
|
|
21
|
+
const cred = await resolveCredential(process.env, stateDir);
|
|
22
|
+
return cred.token ?? null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function defaultClientFactory(stateDir, server) {
|
|
29
|
+
/* THE FOURTH ARGUMENT IS WHY A HOSTED SERVER WORKS AT ALL, and omitting it
|
|
30
|
+
* was silent. A relayed server needs TWO credentials: the MCP one in
|
|
31
|
+
* `X-MCP-Authorization`, and the daemon's own Corenel account token in
|
|
32
|
+
* `Authorization` so the relay knows whose account to rate limit. With no
|
|
33
|
+
* token supplied, authProviderFor takes its signed-out path and sends no
|
|
34
|
+
* Authorization at all -- so api.corenel.ai's own clerkAuth refuses the call,
|
|
35
|
+
* and the daemon reports it as `refused the credential (401)`, which reads as
|
|
36
|
+
* a Microsoft problem and is not one.
|
|
37
|
+
*
|
|
38
|
+
* Passed as a FUNCTION, read per request. That token is a short-lived OAuth
|
|
39
|
+
* access token with a refresh behind it; capturing its value here would work
|
|
40
|
+
* for an hour and then fail with the same misleading 401. */
|
|
41
|
+
/* THE REFRESH CHAIN GETS A DEADLINE; THE CLIENT DOES NOT.
|
|
42
|
+
*
|
|
43
|
+
* An expired credential is refreshed inside `auth.headers()` -- discovery,
|
|
44
|
+
* then a token POST -- and with no deadline a cold authorization server held
|
|
45
|
+
* daemon startup indefinitely. Those requests are never legitimately slow.
|
|
46
|
+
* A tool CALL can be, so the client keeps plain fetch; connect time is bounded
|
|
47
|
+
* per server by `connectAll` instead. */
|
|
48
|
+
return new McpHttpClient(server, authProviderFor(stateDir, server, timedFetch(AUTH_REQUEST_TIMEOUT_MS), () => accountTokenFor(stateDir)));
|
|
49
|
+
}
|
|
50
|
+
/** The longest one server may take to connect -- credential refresh,
|
|
51
|
+
* initialize and tools/list together -- before it is reported as timed out
|
|
52
|
+
* and the daemon moves on without it. Generous for a cold service; short
|
|
53
|
+
* enough that a stalled one reads as a failure, not a hang. */
|
|
54
|
+
export const SERVER_CONNECT_TIMEOUT_MS = 30_000;
|
|
55
|
+
export class McpRegistry {
|
|
56
|
+
stateDir;
|
|
57
|
+
clientFactory;
|
|
58
|
+
connected = new Map();
|
|
59
|
+
statuses = new Map();
|
|
60
|
+
// The order the operator wrote servers in daemon.json, captured once per
|
|
61
|
+
// connectAll() -- `tools()` walks THIS, never `connected`'s Map iteration
|
|
62
|
+
// order. `connectOne` runs concurrently for every server (Promise.all), so
|
|
63
|
+
// Map insertion order is race order: whichever server's initialize+list
|
|
64
|
+
// happened to resolve first. That is fine for `connected` (a lookup table,
|
|
65
|
+
// order never read) but `tools()` is a list a model/doctor/policy consumes
|
|
66
|
+
// in order, and race order means a different tool ordering on every daemon
|
|
67
|
+
// boot -- exactly the kind of bug that surfaces far from here and is never
|
|
68
|
+
// traced back. Configured order is also information the operator supplied
|
|
69
|
+
// deliberately; sorting alphabetically would discard it.
|
|
70
|
+
serverOrder = [];
|
|
71
|
+
// Tool lists seeded from the on-disk cache, with no socket ever opened.
|
|
72
|
+
// Deliberately a THIRD piece of state, not folded into `connected`: a
|
|
73
|
+
// primed server can answer `tools()` but must still go through
|
|
74
|
+
// `ensureConnected` before `call()` will dispatch to it. `connectAll`
|
|
75
|
+
// does not touch this map -- see the comment at its first line for why.
|
|
76
|
+
primed = new Map();
|
|
77
|
+
// One in-flight ensureConnected() attempt per server, so three tool calls
|
|
78
|
+
// that land on the same cold server in the same turn share one connection
|
|
79
|
+
// instead of racing three.
|
|
80
|
+
connecting = new Map();
|
|
81
|
+
constructor(stateDir, clientFactory = defaultClientFactory) {
|
|
82
|
+
this.stateDir = stateDir;
|
|
83
|
+
this.clientFactory = clientFactory;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Connect to every configured server and cache its tools. Never throws --
|
|
87
|
+
* a server that fails to initialize, or fails listing tools, is recorded
|
|
88
|
+
* in `status()` with zero tools and the daemon keeps starting.
|
|
89
|
+
*/
|
|
90
|
+
async connectAll(opts = {}) {
|
|
91
|
+
this.connected.clear();
|
|
92
|
+
this.statuses.clear();
|
|
93
|
+
// Cleared too, deliberately: connectAll is a full, authoritative resync.
|
|
94
|
+
// A primed entry is a cache's word for what a server's tools USED to be;
|
|
95
|
+
// once connectAll has actually tried the server and recorded the truth in
|
|
96
|
+
// `statuses`, leaving a stale primed entry behind would let `tools()`
|
|
97
|
+
// report tools for a server `status()` just said failed to connect --
|
|
98
|
+
// the two would disagree. The daemon never calls primeFromCache at all,
|
|
99
|
+
// so today this is a no-op for it; it matters only if a future caller
|
|
100
|
+
// primes and then does a full connectAll on the same registry.
|
|
101
|
+
this.primed.clear();
|
|
102
|
+
const servers = await readMcpServers(this.stateDir);
|
|
103
|
+
this.serverOrder = servers.map((s) => s.name);
|
|
104
|
+
const timeoutMs = opts.timeoutMs ?? SERVER_CONNECT_TIMEOUT_MS;
|
|
105
|
+
await Promise.all(servers.map((server) => this.connectOne(server, timeoutMs, opts.onServer)));
|
|
106
|
+
}
|
|
107
|
+
/* BOUNDED, AND SETTLED EXACTLY ONCE.
|
|
108
|
+
*
|
|
109
|
+
* Every server connects under one Promise.all, and daemon startup awaits it.
|
|
110
|
+
* With no bound, one stalled server -- measured: an expired Microsoft 365
|
|
111
|
+
* token refreshing against a cold service, still waiting at 60 seconds --
|
|
112
|
+
* left the terminal silent after `mirrors sessions`, indistinguishable from a
|
|
113
|
+
* crash.
|
|
114
|
+
*
|
|
115
|
+
* The deadline covers the whole connect (any credential refresh, initialize,
|
|
116
|
+
* tools/list), not each request, because several individually bounded steps
|
|
117
|
+
* in a row can still add up to a hang. The attempt that loses the race keeps
|
|
118
|
+
* running; its late result is deliberately ignored, so a server reported as
|
|
119
|
+
* timed out cannot quietly become connected afterwards -- status would then
|
|
120
|
+
* tell doctor one thing and the registered tools another. */
|
|
121
|
+
async connectOne(server, timeoutMs, onServer) {
|
|
122
|
+
let timer;
|
|
123
|
+
const deadline = new Promise((resolve) => {
|
|
124
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
125
|
+
});
|
|
126
|
+
const attempt = (async () => {
|
|
127
|
+
const client = this.clientFactory(this.stateDir, server);
|
|
128
|
+
await client.initialize();
|
|
129
|
+
// If this throws after the server already sent tools over the wire,
|
|
130
|
+
// McpHttpClient's `listTools()` still resolves or rejects as ONE
|
|
131
|
+
// array -- there is no partial array to accidentally keep here. Do
|
|
132
|
+
// not refactor this into a loop that pushes tools one at a time; that
|
|
133
|
+
// would reintroduce the exact partial-list bug this module exists to
|
|
134
|
+
// prevent.
|
|
135
|
+
const tools = await client.listTools();
|
|
136
|
+
return { client, tools };
|
|
137
|
+
})();
|
|
138
|
+
// The loser of the race must not surface later as an unhandled rejection.
|
|
139
|
+
attempt.catch(() => { });
|
|
140
|
+
try {
|
|
141
|
+
const outcome = await Promise.race([attempt, deadline]);
|
|
142
|
+
if (outcome === 'timeout') {
|
|
143
|
+
this.statuses.set(server.name, { connected: false, toolCount: 0, error: `timed out after ${timeoutMs / 1000}s` });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.connected.set(server.name, outcome);
|
|
147
|
+
this.statuses.set(server.name, { connected: true, toolCount: outcome.tools.length });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
this.statuses.set(server.name, {
|
|
152
|
+
connected: false,
|
|
153
|
+
toolCount: 0,
|
|
154
|
+
error: err instanceof Error ? err.message : String(err),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
const settled = this.statuses.get(server.name);
|
|
161
|
+
if (settled)
|
|
162
|
+
onServer?.({ server: server.name, ...settled });
|
|
163
|
+
}
|
|
164
|
+
/** Every tool from every connected OR primed server, paired with the server
|
|
165
|
+
* it came from, in CONFIGURED order (`serverOrder`) -- not the order
|
|
166
|
+
* servers happened to finish connecting in, and not insertion order into
|
|
167
|
+
* `primed` either. Within a server, tools stay in the order that server
|
|
168
|
+
* returned them. A live connection wins over a primed (cached) entry for
|
|
169
|
+
* the same server, since it is the fresher truth. A server with neither
|
|
170
|
+
* either failed to connect or reported zero tools -- `status()` tells the
|
|
171
|
+
* two apart. */
|
|
172
|
+
tools() {
|
|
173
|
+
const out = [];
|
|
174
|
+
for (const server of this.serverOrder) {
|
|
175
|
+
const entry = this.connected.get(server);
|
|
176
|
+
const list = entry ? entry.tools : this.primed.get(server);
|
|
177
|
+
if (!list)
|
|
178
|
+
continue;
|
|
179
|
+
for (const tool of list)
|
|
180
|
+
out.push({ server, tool });
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
/** Seed tool lists from the on-disk cache. NO I/O, no sockets -- this is
|
|
185
|
+
* what a terminal calls at startup so that `tools()` can answer
|
|
186
|
+
* immediately, before any server has actually been reached. A primed
|
|
187
|
+
* server is deliberately NOT `connected`: it can answer what tools exist,
|
|
188
|
+
* and cannot call one until `ensureConnected()` has run. */
|
|
189
|
+
primeFromCache(cache) {
|
|
190
|
+
for (const [name, entry] of Object.entries(cache)) {
|
|
191
|
+
this.statuses.set(name, { connected: false, toolCount: entry.tools.length });
|
|
192
|
+
this.primed.set(name, entry.tools);
|
|
193
|
+
if (!this.serverOrder.includes(name))
|
|
194
|
+
this.serverOrder.push(name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
isConnected(server) {
|
|
198
|
+
return this.connected.has(server);
|
|
199
|
+
}
|
|
200
|
+
connectedServers() {
|
|
201
|
+
return this.serverOrder.filter((s) => this.connected.has(s));
|
|
202
|
+
}
|
|
203
|
+
/** Connect ONE server, on demand. Never throws, for the same reason
|
|
204
|
+
* connectAll never throws: a server that is down must not take the
|
|
205
|
+
* process with it. Concurrent callers for the same server share one
|
|
206
|
+
* attempt rather than opening a second socket. */
|
|
207
|
+
async ensureConnected(server) {
|
|
208
|
+
if (this.connected.has(server))
|
|
209
|
+
return;
|
|
210
|
+
const inflight = this.connecting.get(server);
|
|
211
|
+
if (inflight)
|
|
212
|
+
return inflight;
|
|
213
|
+
const attempt = (async () => {
|
|
214
|
+
const servers = await readMcpServers(this.stateDir);
|
|
215
|
+
const def = servers.find((s) => s.name === server);
|
|
216
|
+
if (!def)
|
|
217
|
+
return;
|
|
218
|
+
// A server reached only through ensureConnected (never primed, never
|
|
219
|
+
// through connectAll) has no place in serverOrder yet -- add it now so
|
|
220
|
+
// connectedServers()/tools()/status() all agree on its position rather
|
|
221
|
+
// than silently dropping it.
|
|
222
|
+
if (!this.serverOrder.includes(server))
|
|
223
|
+
this.serverOrder.push(server);
|
|
224
|
+
await this.connectOne(def, SERVER_CONNECT_TIMEOUT_MS);
|
|
225
|
+
})().finally(() => this.connecting.delete(server));
|
|
226
|
+
this.connecting.set(server, attempt);
|
|
227
|
+
return attempt;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Call one tool on one server. Fails immediately, naming the server, when
|
|
231
|
+
* that server never connected -- there is nothing to route the call
|
|
232
|
+
* through, and pretending otherwise would either stall waiting on a
|
|
233
|
+
* connection that will never appear or return an empty result the caller
|
|
234
|
+
* could mistake for a genuine (if empty) tool response.
|
|
235
|
+
*/
|
|
236
|
+
async call(server, tool, args) {
|
|
237
|
+
// A cached-but-cold server (primed, never connected) gets its one socket
|
|
238
|
+
// opened here, on first actual use, rather than failing outright just
|
|
239
|
+
// because nothing had reached for it yet.
|
|
240
|
+
await this.ensureConnected(server);
|
|
241
|
+
const entry = this.connected.get(server);
|
|
242
|
+
if (!entry) {
|
|
243
|
+
throw new Error(`MCP server "${server}" is not connected -- run: corenel mcp doctor`);
|
|
244
|
+
}
|
|
245
|
+
return entry.client.callTool(tool, args);
|
|
246
|
+
}
|
|
247
|
+
/** Per-server connection state, for `doctor` (Task 10) to print. A server
|
|
248
|
+
* with `connected: false` always carries `error` -- there is no code path
|
|
249
|
+
* that records a failure without a reason, because "no tools" alone tells
|
|
250
|
+
* the reader nothing about what to fix. */
|
|
251
|
+
status() {
|
|
252
|
+
/* CONFIGURED order, for the same reason `tools()` uses it. `statuses` fills
|
|
253
|
+
* in whichever order servers settle, and now that a stalled server is cut
|
|
254
|
+
* off at a deadline rather than waited on, that order changes from boot to
|
|
255
|
+
* boot -- doctor would list the same servers differently every run. */
|
|
256
|
+
const out = [];
|
|
257
|
+
for (const server of this.serverOrder) {
|
|
258
|
+
const s = this.statuses.get(server);
|
|
259
|
+
if (s)
|
|
260
|
+
out.push({ server, ...s });
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** How long one refresh-chain request may take before it is abandoned. */
|
|
2
|
+
export declare const AUTH_REQUEST_TIMEOUT_MS = 15000;
|
|
3
|
+
/** Wrap `inner` so every request gives up after `ms`.
|
|
4
|
+
*
|
|
5
|
+
* The caller's own signal still applies: a deadline adds a bound, it never
|
|
6
|
+
* removes the ability to cancel sooner. A timeout is reported as
|
|
7
|
+
* "no answer within Ns" so the error names what happened rather than
|
|
8
|
+
* surfacing a bare AbortError. */
|
|
9
|
+
export declare function timedFetch(ms: number, inner?: typeof fetch): typeof fetch;
|
|
10
|
+
//# sourceMappingURL=timedFetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timedFetch.d.ts","sourceRoot":"","sources":["../src/timedFetch.ts"],"names":[],"mappings":"AAgBA,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C;;;;;mCAKmC;AACnC,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,GAAE,OAAO,KAAa,GAAG,OAAO,KAAK,CAahF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* A fetch with a deadline.
|
|
2
|
+
*
|
|
3
|
+
* Used for the OAuth refresh chain -- discovery, then the token POST -- which
|
|
4
|
+
* runs inside `auth.headers()` whenever a stored credential has expired. That
|
|
5
|
+
* chain carried no signal at all, so an expired Microsoft 365 token refreshing
|
|
6
|
+
* against a cold Cloud Run service could hold a daemon's startup indefinitely
|
|
7
|
+
* (measured: still waiting at 60 seconds; Node's own header wait is five
|
|
8
|
+
* minutes).
|
|
9
|
+
*
|
|
10
|
+
* NOT used for MCP tool calls. A real tool call -- a Graph batch, a long
|
|
11
|
+
* search -- can legitimately take longer than any deadline short enough to
|
|
12
|
+
* make startup feel responsive, and cutting one off would turn a slow answer
|
|
13
|
+
* into a wrong one. Connect time is bounded separately, per server, by the
|
|
14
|
+
* registry.
|
|
15
|
+
*/
|
|
16
|
+
/** How long one refresh-chain request may take before it is abandoned. */
|
|
17
|
+
export const AUTH_REQUEST_TIMEOUT_MS = 15_000;
|
|
18
|
+
/** Wrap `inner` so every request gives up after `ms`.
|
|
19
|
+
*
|
|
20
|
+
* The caller's own signal still applies: a deadline adds a bound, it never
|
|
21
|
+
* removes the ability to cancel sooner. A timeout is reported as
|
|
22
|
+
* "no answer within Ns" so the error names what happened rather than
|
|
23
|
+
* surfacing a bare AbortError. */
|
|
24
|
+
export function timedFetch(ms, inner = fetch) {
|
|
25
|
+
return (async (input, init = {}) => {
|
|
26
|
+
const deadline = AbortSignal.timeout(ms);
|
|
27
|
+
const signal = init.signal ? AbortSignal.any([init.signal, deadline]) : deadline;
|
|
28
|
+
try {
|
|
29
|
+
return await inner(input, { ...init, signal });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (deadline.aborted && !init.signal?.aborted) {
|
|
33
|
+
throw new Error(`no answer within ${ms / 1000}s`, { cause: err });
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=timedFetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timedFetch.js","sourceRoot":"","sources":["../src/timedFetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C;;;;;mCAKmC;AACnC,MAAM,UAAU,UAAU,CAAC,EAAU,EAAE,QAAsB,KAAK;IAChE,OAAO,CAAC,KAAK,EAAE,KAAkC,EAAE,OAAoB,EAAE,EAAE,EAAE;QAC3E,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACjF,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,QAAQ,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;gBAC9C,MAAM,IAAI,KAAK,CAAC,oBAAoB,EAAE,GAAG,IAAI,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACpE,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,CAAiB,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolNames.d.ts","sourceRoot":"","sources":["../src/toolNames.ts"],"names":[],"mappings":"AAYA,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Re-exports @corenel/protocol's MCP tool-naming scheme (wire name <->
|
|
2
|
+
* address). The encoding itself moved to packages/protocol/mcpToolNames.ts
|
|
3
|
+
* so packages/harness/tools/namespaces.ts -- shared with the browser, and
|
|
4
|
+
* not allowed to depend on this Node-only package -- can produce the SAME
|
|
5
|
+
* address `asAgentTools` puts on each `AgentTool.name`, rather than two
|
|
6
|
+
* independent implementations of the same scheme silently drifting apart.
|
|
7
|
+
* See that file's module comment for the full design (escaping choices,
|
|
8
|
+
* the refuse-vs-escape split between address and wire name, and why the
|
|
9
|
+
* legacy `mcp__server__tool` shape is handled separately).
|
|
10
|
+
*
|
|
11
|
+
* This module exists so nothing under packages/sidecar/src/mcp had to change
|
|
12
|
+
* its import path. */
|
|
13
|
+
export { mcpToolAddress, parseMcpToolAddress, mcpWireName, parseMcpWireName, canEncodeMcpWireName, looksLikeMcpWireName, } from '@corenel/protocol';
|
|
14
|
+
//# sourceMappingURL=toolNames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolNames.js","sourceRoot":"","sources":["../src/toolNames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;sBAWsB;AACtB,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** One configured MCP server. No secret material -- see McpCredential. */
|
|
2
|
+
export interface McpServer {
|
|
3
|
+
name: string;
|
|
4
|
+
url: string;
|
|
5
|
+
/** Reached through OUR relay (api.corenel.ai/mcp/hosted/<name>) rather than
|
|
6
|
+
* directly. Such a call carries TWO credentials: the daemon's own account
|
|
7
|
+
* token in `Authorization`, so the API can attribute and rate-limit it, and
|
|
8
|
+
* the MCP server's credential in `X-MCP-Authorization`, which the relay
|
|
9
|
+
* swaps back on the way out.
|
|
10
|
+
*
|
|
11
|
+
* A FLAG ON THE SERVER, not a credential kind, because "how do I
|
|
12
|
+
* authenticate to this server" and "how do I reach it" are different
|
|
13
|
+
* questions. As a kind it would need a `-headers` and an `-oauth` variant
|
|
14
|
+
* and another for every kind added later; as a flag it composes with all
|
|
15
|
+
* of them. */
|
|
16
|
+
hosted?: boolean;
|
|
17
|
+
/** Where to SIGN IN, when that is not where calls go.
|
|
18
|
+
*
|
|
19
|
+
* A relayed server splits in two: calls go through the Corenel relay so it
|
|
20
|
+
* can rate limit them per account, while the OAuth dance -- discovery,
|
|
21
|
+
* /authorize, /token -- happens directly against the hosted service, whose
|
|
22
|
+
* gate leaves those paths public so nobody sits in the middle of an auth
|
|
23
|
+
* exchange.
|
|
24
|
+
*
|
|
25
|
+
* Absent for every other server, where `url` answers both questions. When
|
|
26
|
+
* absent the login flow uses `url`, which is what it always did. */
|
|
27
|
+
authUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
/** A credential presented as static request headers -- an API key, a bearer
|
|
30
|
+
* token issued out of band, a basic-auth value. Whatever the server wants,
|
|
31
|
+
* the header text is the whole of it. */
|
|
32
|
+
export interface McpHeadersCredential {
|
|
33
|
+
kind: 'headers';
|
|
34
|
+
headers: {
|
|
35
|
+
name: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A credential from an OAuth flow against the MCP server's own auth server.
|
|
41
|
+
*
|
|
42
|
+
* `refreshToken` is itself a credential -- long-lived, and the thing an
|
|
43
|
+
* attacker who reads this file would actually want -- so every reader and
|
|
44
|
+
* writer that touches it must treat it exactly as it treats accessToken:
|
|
45
|
+
* never logged, never printed, never mirrored into daemon.json.
|
|
46
|
+
*
|
|
47
|
+
* `expiresAt` absent means the issuer did not say, matching the same
|
|
48
|
+
* convention as tools-node's StoredCredential -- NOT "does not expire".
|
|
49
|
+
*/
|
|
50
|
+
export interface McpOAuthCredential {
|
|
51
|
+
kind: 'oauth';
|
|
52
|
+
accessToken: string;
|
|
53
|
+
refreshToken?: string;
|
|
54
|
+
/** Epoch ms. Absent means the issuer did not say. */
|
|
55
|
+
expiresAt?: number;
|
|
56
|
+
clientId: string;
|
|
57
|
+
clientSecret?: string;
|
|
58
|
+
authServer: string;
|
|
59
|
+
scope?: string;
|
|
60
|
+
}
|
|
61
|
+
export type McpCredential = McpHeadersCredential | McpOAuthCredential;
|
|
62
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAkBA,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;;;mBAUe;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;;;;;;yEASqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;0CAE0C;AAC1C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5C;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,aAAa,GAAG,oBAAoB,GAAG,kBAAkB,CAAC"}
|