@pi-archimedes/mcp 2.3.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 +21 -0
- package/README.md +170 -0
- package/package.json +39 -0
- package/src/auth-flow.test.ts +583 -0
- package/src/auth-flow.ts +310 -0
- package/src/auth-run.test.ts +309 -0
- package/src/auth-run.ts +146 -0
- package/src/auth-storage.test.ts +338 -0
- package/src/auth-storage.ts +330 -0
- package/src/auto-auth.test.ts +231 -0
- package/src/auto-auth.ts +135 -0
- package/src/callback-server.test.ts +446 -0
- package/src/callback-server.ts +538 -0
- package/src/commands-auth.test.ts +320 -0
- package/src/commands-auth.ts +128 -0
- package/src/commands.test.ts +834 -0
- package/src/commands.ts +424 -0
- package/src/config-write.test.ts +213 -0
- package/src/config-write.ts +207 -0
- package/src/config.test.ts +468 -0
- package/src/config.ts +278 -0
- package/src/direct-tools.test.ts +473 -0
- package/src/direct-tools.ts +250 -0
- package/src/host-configs.test.ts +231 -0
- package/src/host-configs.ts +106 -0
- package/src/index.test.ts +689 -0
- package/src/index.ts +146 -0
- package/src/lifecycle.test.ts +274 -0
- package/src/lifecycle.ts +77 -0
- package/src/metadata-cache.test.ts +383 -0
- package/src/metadata-cache.ts +231 -0
- package/src/npx-resolver.test.ts +142 -0
- package/src/npx-resolver.ts +126 -0
- package/src/oauth-provider.test.ts +404 -0
- package/src/oauth-provider.ts +197 -0
- package/src/oauth-types.ts +54 -0
- package/src/panel-rows.ts +210 -0
- package/src/panel.test.ts +298 -0
- package/src/panel.ts +742 -0
- package/src/proxy-tool.ts +524 -0
- package/src/renderer.test.ts +326 -0
- package/src/renderer.ts +239 -0
- package/src/schema-validator.test.ts +56 -0
- package/src/schema-validator.ts +42 -0
- package/src/server-client.test.ts +1001 -0
- package/src/server-client.ts +576 -0
- package/src/server-manager.ts +139 -0
- package/src/setup-panel.test.ts +162 -0
- package/src/setup-panel.ts +715 -0
- package/src/tool-naming.test.ts +168 -0
- package/src/tool-naming.ts +114 -0
- package/src/types.ts +162 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_NAMES,
|
|
4
|
+
findFormattingCollisions,
|
|
5
|
+
formatToolName,
|
|
6
|
+
getServerPrefix,
|
|
7
|
+
matchRawToolName,
|
|
8
|
+
resolveServerFromToolName,
|
|
9
|
+
sanitizeServerPrefix,
|
|
10
|
+
} from "./tool-naming.js";
|
|
11
|
+
|
|
12
|
+
describe("sanitizeServerPrefix", () => {
|
|
13
|
+
it("keeps alphanumerics, underscores, and dashes untouched", () => {
|
|
14
|
+
expect(sanitizeServerPrefix("my-server_1")).toBe("my-server_1");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("hex-encodes disallowed characters with _<hex>_ wrappers", () => {
|
|
18
|
+
expect(sanitizeServerPrefix("a.b")).toBe("a_2e_b");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("hex-encodes multiple disallowed characters", () => {
|
|
22
|
+
// "!" = 0x21, "." = 0x2e
|
|
23
|
+
expect(sanitizeServerPrefix("a!b.c")).toBe("a_21_b_2e_c");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("getServerPrefix", () => {
|
|
28
|
+
it("server mode sanitizes the raw name", () => {
|
|
29
|
+
expect(getServerPrefix("my.server", "server")).toBe("my_2e_server");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("none mode returns an empty prefix", () => {
|
|
33
|
+
expect(getServerPrefix("anything", "none")).toBe("");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("short mode strips a trailing -mcp suffix before sanitizing", () => {
|
|
37
|
+
expect(getServerPrefix("filesystem-mcp", "short")).toBe("filesystem");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("short mode strips a bare mcp suffix (case-insensitive)", () => {
|
|
41
|
+
expect(getServerPrefix("GitHubMCP", "short")).toBe("GitHub");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("mcp mode wraps the sanitized name in mcp__", () => {
|
|
45
|
+
expect(getServerPrefix("my.server", "mcp")).toBe("mcp__my_2e_server");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("formatToolName", () => {
|
|
50
|
+
it("builds <prefix>_<tool> for server mode", () => {
|
|
51
|
+
expect(formatToolName("search", "github", "server")).toBe("github_search");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns the bare tool name in none mode", () => {
|
|
55
|
+
expect(formatToolName("search", "github", "none")).toBe("search");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("converts dots in tool names to underscores", () => {
|
|
59
|
+
expect(formatToolName("a.b.c", "srv", "server")).toBe("srv_a_b_c");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("combines mcp mode with dot conversion", () => {
|
|
63
|
+
expect(formatToolName("a.b", "fs-mcp", "mcp")).toBe("mcp__fs-mcp_a_b");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("resolveServerFromToolName", () => {
|
|
68
|
+
it("resolves the owning server", () => {
|
|
69
|
+
const servers = [
|
|
70
|
+
{ name: "github", prefix: "server" as const },
|
|
71
|
+
{ name: "jira", prefix: "server" as const },
|
|
72
|
+
];
|
|
73
|
+
expect(resolveServerFromToolName("github_search", servers)).toBe("github");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("computes each server's prefix using ITS OWN mode", () => {
|
|
77
|
+
// In "short" mode, "x-mcp" has prefix "x", so "x_tool" resolves to it.
|
|
78
|
+
const servers = [{ name: "x-mcp", prefix: "short" as const }];
|
|
79
|
+
expect(resolveServerFromToolName("x_tool", servers)).toBe("x-mcp");
|
|
80
|
+
// In "server" mode the same server would be "x-mcp", so "x_tool" must NOT match.
|
|
81
|
+
const servers2 = [{ name: "x-mcp", prefix: "server" as const }];
|
|
82
|
+
expect(resolveServerFromToolName("x_tool", servers2)).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("longest matching prefix wins", () => {
|
|
86
|
+
const servers = [
|
|
87
|
+
{ name: "a", prefix: "server" as const },
|
|
88
|
+
{ name: "a_b", prefix: "server" as const },
|
|
89
|
+
];
|
|
90
|
+
// "a_b_x" matches both "a_" (len 1) and "a_b_" (len 3) → "a_b" wins
|
|
91
|
+
expect(resolveServerFromToolName("a_b_x", servers)).toBe("a_b");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns undefined when two servers tie on the longest matching prefix", () => {
|
|
95
|
+
const servers = [
|
|
96
|
+
{ name: "github", prefix: "server" as const },
|
|
97
|
+
{ name: "github-mcp", prefix: "short" as const }, // also resolves to "github"
|
|
98
|
+
];
|
|
99
|
+
expect(resolveServerFromToolName("github_search", servers)).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns undefined when no server matches", () => {
|
|
103
|
+
const servers = [{ name: "github", prefix: "server" as const }];
|
|
104
|
+
expect(resolveServerFromToolName("read", servers)).toBeUndefined();
|
|
105
|
+
expect(resolveServerFromToolName("nonesrv_tool", [{ name: "s", prefix: "none" as const }])).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("matchRawToolName", () => {
|
|
110
|
+
it("resolves the raw dotted tool name from its sanitized prefixed name", () => {
|
|
111
|
+
const tools = [{ name: "a.b" }, { name: "x.y" }, { name: "plain" }];
|
|
112
|
+
expect(matchRawToolName("srv_a_b", "srv", "server", tools)).toBe("a.b");
|
|
113
|
+
expect(matchRawToolName("srv_plain", "srv", "server", tools)).toBe("plain");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("uses the server's OWN prefix mode", () => {
|
|
117
|
+
const tools = [{ name: "a.b" }];
|
|
118
|
+
// short mode: "github-mcp" → "github"
|
|
119
|
+
expect(matchRawToolName("github_a_b", "github-mcp", "short", tools)).toBe("a.b");
|
|
120
|
+
// mcp mode: "mcp__srv"
|
|
121
|
+
expect(matchRawToolName("mcp__srv_a_b", "srv", "mcp", tools)).toBe("a.b");
|
|
122
|
+
// none mode: bare (but dot-sanitized) name
|
|
123
|
+
expect(matchRawToolName("a_b", "srv", "none", tools)).toBe("a.b");
|
|
124
|
+
// A name formatted under one mode must not match under another
|
|
125
|
+
expect(matchRawToolName("github_a_b", "github-mcp", "server", tools)).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns undefined when no tool formats to the given name", () => {
|
|
129
|
+
expect(matchRawToolName("srv_nope", "srv", "server", [{ name: "a.b" }])).toBeUndefined();
|
|
130
|
+
expect(matchRawToolName("srv_a_b", "srv", "server", [])).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("findFormattingCollisions", () => {
|
|
135
|
+
it("detects raw names that format to the same final name", () => {
|
|
136
|
+
const tools = [{ name: "a.b" }, { name: "a_b" }, { name: "plain" }];
|
|
137
|
+
expect(findFormattingCollisions("srv", "server", tools)).toEqual([
|
|
138
|
+
{ finalName: "srv_a_b", rawNames: ["a.b", "a_b"] },
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns [] when all formatted names are unique", () => {
|
|
143
|
+
const tools = [{ name: "a.b" }, { name: "x.y" }, { name: "plain" }];
|
|
144
|
+
expect(findFormattingCollisions("srv", "server", tools)).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("detects collisions in none mode (bare sanitized names)", () => {
|
|
148
|
+
expect(findFormattingCollisions("srv", "none", [{ name: "a.b" }, { name: "a_b" }])).toEqual([
|
|
149
|
+
{ finalName: "a_b", rawNames: ["a.b", "a_b"] },
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("excludes raw names that format distinctly from the collision", () => {
|
|
154
|
+
const tools = [{ name: "a.b" }, { name: "a_b" }, { name: "aB" }];
|
|
155
|
+
// "aB" formats to "mcp__srv_aB" — NOT part of the collision group
|
|
156
|
+
expect(findFormattingCollisions("srv", "mcp", tools)).toEqual([
|
|
157
|
+
{ finalName: "mcp__srv_a_b", rawNames: ["a.b", "a_b"] },
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("BUILTIN_NAMES", () => {
|
|
163
|
+
it("contains the pi builtin tool names including mcp", () => {
|
|
164
|
+
for (const name of ["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]) {
|
|
165
|
+
expect(BUILTIN_NAMES.has(name)).toBe(true);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ToolPrefix } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Sanitize a server name: keep [A-Za-z0-9_-], replace others with _<hex>_ */
|
|
4
|
+
export function sanitizeServerPrefix(name: string): string {
|
|
5
|
+
return name.replace(/[^A-Za-z0-9_-]/g, (c) => `_${c.codePointAt(0)!.toString(16)}_`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getServerPrefix(serverName: string, prefix: ToolPrefix): string {
|
|
9
|
+
switch (prefix) {
|
|
10
|
+
case "none": return "";
|
|
11
|
+
case "short": return sanitizeServerPrefix(serverName.replace(/-?mcp$/i, ""));
|
|
12
|
+
case "mcp": return `mcp__${sanitizeServerPrefix(serverName)}`;
|
|
13
|
+
case "server":
|
|
14
|
+
default: return sanitizeServerPrefix(serverName);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Build prefixed tool name: <prefix>_<toolName with . → _> */
|
|
19
|
+
export function formatToolName(toolName: string, serverName: string, prefix: ToolPrefix): string {
|
|
20
|
+
const p = getServerPrefix(serverName, prefix);
|
|
21
|
+
const sanitized = toolName.replace(/\./g, "_");
|
|
22
|
+
return p ? `${p}_${sanitized}` : sanitized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Inverse: find server owning a prefixed tool name by longest matching prefix.
|
|
27
|
+
* Returns undefined if no match OR if two servers tie for the longest matching prefix (ambiguous).
|
|
28
|
+
* IMPORTANT: compute each server's prefix using ITS OWN mode, not a hardcoded "server".
|
|
29
|
+
*/
|
|
30
|
+
export function resolveServerFromToolName(
|
|
31
|
+
prefixedName: string,
|
|
32
|
+
servers: Array<{ name: string; prefix: ToolPrefix }>,
|
|
33
|
+
): string | undefined {
|
|
34
|
+
const matches: Array<{ name: string; prefixLen: number }> = [];
|
|
35
|
+
for (const { name, prefix } of servers) {
|
|
36
|
+
const p = getServerPrefix(name, prefix);
|
|
37
|
+
if (p && prefixedName.startsWith(`${p}_`)) matches.push({ name, prefixLen: p.length });
|
|
38
|
+
}
|
|
39
|
+
if (matches.length === 0) return undefined;
|
|
40
|
+
matches.sort((a, b) => b.prefixLen - a.prefixLen);
|
|
41
|
+
// Ambiguous if the top two share the winning prefix length
|
|
42
|
+
if (matches.length > 1 && matches[0]!.prefixLen === matches[1]!.prefixLen) return undefined;
|
|
43
|
+
return matches[0]!.name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Group a server's raw tool names by their final formatted name, returning
|
|
48
|
+
* only the groups with 2+ members. formatToolName is NOT injective: the
|
|
49
|
+
* `.`→`_` sanitization means e.g. raw names "a.b" and "a_b" both format to
|
|
50
|
+
* "srv_a_b" — such groups are genuinely ambiguous, and name resolution is
|
|
51
|
+
* first-match-by-list-order.
|
|
52
|
+
*/
|
|
53
|
+
export function findFormattingCollisions(
|
|
54
|
+
serverName: string,
|
|
55
|
+
prefix: ToolPrefix,
|
|
56
|
+
tools: Array<{ name: string }>,
|
|
57
|
+
): Array<{ finalName: string; rawNames: string[] }> {
|
|
58
|
+
const byFinal = new Map<string, string[]>();
|
|
59
|
+
for (const t of tools) {
|
|
60
|
+
const finalName = formatToolName(t.name, serverName, prefix);
|
|
61
|
+
const names = byFinal.get(finalName);
|
|
62
|
+
if (names) names.push(t.name);
|
|
63
|
+
else byFinal.set(finalName, [t.name]);
|
|
64
|
+
}
|
|
65
|
+
return [...byFinal.entries()]
|
|
66
|
+
.filter(([, rawNames]) => rawNames.length > 1)
|
|
67
|
+
.map(([finalName, rawNames]) => ({ finalName, rawNames }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Given a final (possibly prefixed) tool name, find the raw server tool name
|
|
72
|
+
* that formats to it under the server's own prefix mode. Returns undefined
|
|
73
|
+
* when no tool formats to the given name (e.g. the tool list is stale) —
|
|
74
|
+
* callers may then fall back to prefix-stripping, which is only lossless for
|
|
75
|
+
* dot-free tool names (`a.b` → `a_b` is NOT reversible by slicing).
|
|
76
|
+
*
|
|
77
|
+
* NOTE: because formatToolName is not injective ("a.b" and "a_b" format to
|
|
78
|
+
* the same final name), a matching final name may correspond to MULTIPLE
|
|
79
|
+
* raw names; the FIRST match in the provided tool list wins, silently. Use
|
|
80
|
+
* findFormattingCollisions to detect and surface such ambiguity.
|
|
81
|
+
*/
|
|
82
|
+
export function matchRawToolName(
|
|
83
|
+
finalName: string,
|
|
84
|
+
serverName: string,
|
|
85
|
+
prefix: ToolPrefix,
|
|
86
|
+
tools: Array<{ name: string }>,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
for (const t of tools) {
|
|
89
|
+
if (formatToolName(t.name, serverName, prefix) === finalName) return t.name;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a server reference to a configured server name. Accepts an exact
|
|
96
|
+
* server name, a final prefixed tool name (delegated to
|
|
97
|
+
* resolveServerFromToolName), or a bare tool-name prefix (e.g. "github" for
|
|
98
|
+
* server "github-mcp" under "short" mode).
|
|
99
|
+
*/
|
|
100
|
+
export function resolveServerRef(
|
|
101
|
+
ref: string,
|
|
102
|
+
servers: Array<{ name: string; prefix: ToolPrefix }>,
|
|
103
|
+
): string | undefined {
|
|
104
|
+
const byToolName = resolveServerFromToolName(ref, servers);
|
|
105
|
+
if (byToolName) return byToolName;
|
|
106
|
+
// Exact bare-prefix match (only unambiguous single matches are accepted)
|
|
107
|
+
const exact = servers.filter((s) => getServerPrefix(s.name, s.prefix) === ref);
|
|
108
|
+
if (exact.length === 1) return exact[0]!.name;
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const BUILTIN_NAMES = new Set([
|
|
113
|
+
"read", "bash", "edit", "write", "grep", "find", "ls", "mcp",
|
|
114
|
+
]);
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Strategy for prefixing exposed tool names */
|
|
2
|
+
export type ToolPrefix = "server" | "none" | "short" | "mcp";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lifecycle & tooling settings shared by both stdio and HTTP server defs.
|
|
6
|
+
* These can appear per-server in mcp.json files.
|
|
7
|
+
*/
|
|
8
|
+
export interface SharedServerSettings {
|
|
9
|
+
/** Connection lifecycle (default: keep-alive) */
|
|
10
|
+
lifecycle?: "keep-alive" | "lazy" | "lazy-keep-alive" | "eager";
|
|
11
|
+
/** Idle timeout in minutes; 0 disables */
|
|
12
|
+
idleTimeout?: number;
|
|
13
|
+
/** Per-request timeout in milliseconds */
|
|
14
|
+
requestTimeoutMs?: number;
|
|
15
|
+
/** Expose direct tools; true = all, string[] = subset of tool names */
|
|
16
|
+
directTools?: boolean | string[];
|
|
17
|
+
/** Only expose these tools (whitelist) */
|
|
18
|
+
includeTools?: string[];
|
|
19
|
+
/** Never expose these tools (blacklist) */
|
|
20
|
+
excludeTools?: string[];
|
|
21
|
+
/** Tool name prefix strategy (default: "server") */
|
|
22
|
+
toolPrefix?: ToolPrefix;
|
|
23
|
+
/** Expose resources as a list_resources tool */
|
|
24
|
+
exposeResources?: boolean;
|
|
25
|
+
/** Verbose logging for this server */
|
|
26
|
+
debug?: boolean;
|
|
27
|
+
/** Pin the MCP protocol version used for this server */
|
|
28
|
+
protocolVersion?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A stdio-based MCP server (spawns a child process) */
|
|
32
|
+
export interface StdioServerDef extends SharedServerSettings {
|
|
33
|
+
type?: "stdio";
|
|
34
|
+
command: string;
|
|
35
|
+
args?: string[];
|
|
36
|
+
env?: Record<string, string>;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** OAuth 2.1 settings for an HTTP MCP server (see plan-026 / ADR 0001) */
|
|
42
|
+
export interface McpOAuthConfig {
|
|
43
|
+
grantType?: "authorization_code" | "client_credentials"; // default authorization_code
|
|
44
|
+
clientId?: string;
|
|
45
|
+
clientSecret?: string; // literal only (no "!command" resolution — see ADR/scope)
|
|
46
|
+
scope?: string;
|
|
47
|
+
redirectUri?: string; // pre-registered clients only
|
|
48
|
+
clientName?: string;
|
|
49
|
+
authorizationServerUrl?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The full set of known `McpOAuthConfig` fields, in no particular order. */
|
|
53
|
+
export const OAUTH_CONFIG_FIELDS = [
|
|
54
|
+
"grantType",
|
|
55
|
+
"clientId",
|
|
56
|
+
"clientSecret",
|
|
57
|
+
"scope",
|
|
58
|
+
"redirectUri",
|
|
59
|
+
"clientName",
|
|
60
|
+
"authorizationServerUrl",
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
/** An HTTP-based MCP server (Streamable HTTP or SSE) */
|
|
64
|
+
export interface HttpServerDef extends SharedServerSettings {
|
|
65
|
+
/** Informational only — the transport is chosen by def shape: a `url` server
|
|
66
|
+
* connects via StreamableHTTP (with SSE fallback for legacy servers). */
|
|
67
|
+
type?: "http" | "sse";
|
|
68
|
+
url: string;
|
|
69
|
+
/** Bearer token, the "oauth" string (grant-type defaults), or a full OAuth config */
|
|
70
|
+
auth?: { token: string } | "oauth" | McpOAuthConfig;
|
|
71
|
+
/** Extra HTTP headers sent with every request */
|
|
72
|
+
headers?: Record<string, string>;
|
|
73
|
+
/** Name of an environment variable holding the bearer token */
|
|
74
|
+
bearerTokenEnv?: string;
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type ServerDef = StdioServerDef | HttpServerDef;
|
|
79
|
+
|
|
80
|
+
export interface McpFileConfig {
|
|
81
|
+
mcpServers?: Record<string, ServerDef>;
|
|
82
|
+
settings?: McpFileSettings;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Settings that can appear in the mcp config files (not archimedes.mcp) */
|
|
86
|
+
export interface McpFileSettings {
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Settings read from archimedes.mcp in settings.json */
|
|
91
|
+
export interface McpConfig {
|
|
92
|
+
/** Show direct tools per server in the tool list (default: true) */
|
|
93
|
+
directTools: boolean;
|
|
94
|
+
/** Tool name prefix strategy (default: "server") */
|
|
95
|
+
toolPrefix: ToolPrefix;
|
|
96
|
+
/** Idle timeout in minutes (default: 10) */
|
|
97
|
+
idleTimeout: number;
|
|
98
|
+
/** Warn when a server exposes many direct tools (default: true) */
|
|
99
|
+
warnOnLargeDirectTools: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* When a tool call reaches a server in the `needs-auth` state, trigger the
|
|
102
|
+
* interactive OAuth flow inline (single entry point: ServerClient
|
|
103
|
+
* .authenticate) instead of returning guidance to run `/mcp auth`.
|
|
104
|
+
* (default: false — guidance only)
|
|
105
|
+
*/
|
|
106
|
+
autoAuth: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const DEFAULT_MCP_CONFIG: McpConfig = {
|
|
110
|
+
directTools: true,
|
|
111
|
+
toolPrefix: "server",
|
|
112
|
+
idleTimeout: 10,
|
|
113
|
+
warnOnLargeDirectTools: true,
|
|
114
|
+
autoAuth: false,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const MCP_NAMESPACE = "archimedes.mcp";
|
|
118
|
+
|
|
119
|
+
/** A single cached tool definition from a server */
|
|
120
|
+
export interface CachedTool {
|
|
121
|
+
name: string;
|
|
122
|
+
description?: string;
|
|
123
|
+
inputSchema: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Cached metadata for one server */
|
|
127
|
+
export interface ServerCacheEntry {
|
|
128
|
+
/** Hash of the server definition at cache time */
|
|
129
|
+
configHash: string;
|
|
130
|
+
tools: CachedTool[];
|
|
131
|
+
resources: Array<{ uri: string; name?: string; description?: string; mimeType?: string }>;
|
|
132
|
+
prompts?: Array<{ name: string; description?: string }>;
|
|
133
|
+
/** Server-level instructions from initialize */
|
|
134
|
+
instructions?: string;
|
|
135
|
+
/** Epoch milliseconds when the entry was written */
|
|
136
|
+
cachedAt: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Persisted connection outcome for one server (ADR 0004). */
|
|
140
|
+
export interface ServerOutcomeRecord {
|
|
141
|
+
status: "connected" | "needs-auth" | "error";
|
|
142
|
+
/** Error text for needs-auth/error outcomes (first line). */
|
|
143
|
+
error?: string;
|
|
144
|
+
/** Epoch milliseconds when the outcome was recorded. */
|
|
145
|
+
at: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** On-disk shape of the metadata cache */
|
|
149
|
+
export interface MetadataCache {
|
|
150
|
+
version: number;
|
|
151
|
+
servers: Record<string, ServerCacheEntry>;
|
|
152
|
+
/**
|
|
153
|
+
* Last connection outcome per server (ADR 0004). Additive: old cache
|
|
154
|
+
* files lack the field — a missing key means "not verified", so there is
|
|
155
|
+
* no CACHE_VERSION bump. `loadMetadataCache` must round-trip it: dropping
|
|
156
|
+
* the field would silence every `saveServerCache` rewrite.
|
|
157
|
+
*/
|
|
158
|
+
serverStatuses?: Record<string, ServerOutcomeRecord>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const CACHE_VERSION = 1;
|
|
162
|
+
export const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|