@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.
Files changed (74) hide show
  1. package/LICENSE +93 -0
  2. package/dist/asAgentTools.d.ts +12 -0
  3. package/dist/asAgentTools.d.ts.map +1 -0
  4. package/dist/asAgentTools.js +71 -0
  5. package/dist/asAgentTools.js.map +1 -0
  6. package/dist/auth.d.ts +25 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +97 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/cache.d.ts +17 -0
  11. package/dist/cache.d.ts.map +1 -0
  12. package/dist/cache.js +81 -0
  13. package/dist/cache.js.map +1 -0
  14. package/dist/config.d.ts +71 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +239 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/gatedAgentTools.d.ts +12 -0
  19. package/dist/gatedAgentTools.d.ts.map +1 -0
  20. package/dist/gatedAgentTools.js +36 -0
  21. package/dist/gatedAgentTools.js.map +1 -0
  22. package/dist/httpClient.d.ts +47 -0
  23. package/dist/httpClient.d.ts.map +1 -0
  24. package/dist/httpClient.js +196 -0
  25. package/dist/httpClient.js.map +1 -0
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/oauth/discovery.d.ts +55 -0
  31. package/dist/oauth/discovery.d.ts.map +1 -0
  32. package/dist/oauth/discovery.js +211 -0
  33. package/dist/oauth/discovery.js.map +1 -0
  34. package/dist/oauth/login.d.ts +41 -0
  35. package/dist/oauth/login.d.ts.map +1 -0
  36. package/dist/oauth/login.js +206 -0
  37. package/dist/oauth/login.js.map +1 -0
  38. package/dist/oauth/loopback.d.ts +58 -0
  39. package/dist/oauth/loopback.d.ts.map +1 -0
  40. package/dist/oauth/loopback.js +165 -0
  41. package/dist/oauth/loopback.js.map +1 -0
  42. package/dist/oauth/pkce.d.ts +45 -0
  43. package/dist/oauth/pkce.d.ts.map +1 -0
  44. package/dist/oauth/pkce.js +67 -0
  45. package/dist/oauth/pkce.js.map +1 -0
  46. package/dist/oauth/register.d.ts +34 -0
  47. package/dist/oauth/register.d.ts.map +1 -0
  48. package/dist/oauth/register.js +57 -0
  49. package/dist/oauth/register.js.map +1 -0
  50. package/dist/oauth/tokens.d.ts +62 -0
  51. package/dist/oauth/tokens.d.ts.map +1 -0
  52. package/dist/oauth/tokens.js +118 -0
  53. package/dist/oauth/tokens.js.map +1 -0
  54. package/dist/policy.d.ts +10 -0
  55. package/dist/policy.d.ts.map +1 -0
  56. package/dist/policy.js +71 -0
  57. package/dist/policy.js.map +1 -0
  58. package/dist/registry.d.ts +124 -0
  59. package/dist/registry.d.ts.map +1 -0
  60. package/dist/registry.js +265 -0
  61. package/dist/registry.js.map +1 -0
  62. package/dist/timedFetch.d.ts +10 -0
  63. package/dist/timedFetch.d.ts.map +1 -0
  64. package/dist/timedFetch.js +39 -0
  65. package/dist/timedFetch.js.map +1 -0
  66. package/dist/toolNames.d.ts +2 -0
  67. package/dist/toolNames.d.ts.map +1 -0
  68. package/dist/toolNames.js +14 -0
  69. package/dist/toolNames.js.map +1 -0
  70. package/dist/types.d.ts +62 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +19 -0
  73. package/dist/types.js.map +1 -0
  74. 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"}
@@ -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,2 @@
1
+ export { mcpToolAddress, parseMcpToolAddress, mcpWireName, parseMcpWireName, canEncodeMcpWireName, looksLikeMcpWireName, } from '@corenel/protocol';
2
+ //# sourceMappingURL=toolNames.d.ts.map
@@ -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"}
@@ -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"}