@ikarmic/forge-core 5.0.0 → 5.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/dist/agent/agent.d.ts +8 -0
- package/dist/agent/agent.js +123 -22
- package/dist/agent/background.d.ts +58 -0
- package/dist/agent/background.js +139 -0
- package/dist/agent/enhance-prompt.d.ts +3 -1
- package/dist/agent/enhance-prompt.js +6 -2
- package/dist/agent/goal.d.ts +54 -0
- package/dist/agent/goal.js +65 -0
- package/dist/agents/agent-files.d.ts +43 -0
- package/dist/agents/agent-files.js +109 -0
- package/dist/checkpoints/store.js +5 -1
- package/dist/commands/custom-commands.d.ts +9 -4
- package/dist/commands/custom-commands.js +13 -2
- package/dist/config-file.d.ts +43 -0
- package/dist/config-file.js +249 -5
- package/dist/config-merge.d.ts +13 -0
- package/dist/config-merge.js +67 -0
- package/dist/config-schema.d.ts +340 -0
- package/dist/config-schema.js +126 -0
- package/dist/config.js +2 -1
- package/dist/context/compaction.js +4 -0
- package/dist/context/mask.js +1 -1
- package/dist/custom-providers.d.ts +95 -0
- package/dist/custom-providers.js +120 -0
- package/dist/git/auto-commit.d.ts +2 -2
- package/dist/git/auto-commit.js +5 -4
- package/dist/github/github-tools.d.ts +2 -2
- package/dist/hooks/runner.js +3 -2
- package/dist/import/claude-code.d.ts +39 -0
- package/dist/import/claude-code.js +254 -0
- package/dist/index.d.ts +32 -4
- package/dist/index.js +24 -3
- package/dist/lsp/client.d.ts +5 -0
- package/dist/lsp/client.js +39 -3
- package/dist/mcp/index.d.ts +5 -1
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/manager.d.ts +17 -3
- package/dist/mcp/manager.js +27 -2
- package/dist/mcp/marketplace.d.ts +2 -0
- package/dist/mcp/marketplace.js +10 -4
- package/dist/mcp/mcp-client.d.ts +12 -2
- package/dist/mcp/mcp-client.js +74 -17
- package/dist/mcp/mcp-config.d.ts +2 -0
- package/dist/mcp/mcp-config.js +1 -0
- package/dist/mcp/oauth.d.ts +55 -0
- package/dist/mcp/oauth.js +197 -0
- package/dist/mcp/resource-tools.d.ts +8 -0
- package/dist/mcp/resource-tools.js +56 -0
- package/dist/memory/session-digest.d.ts +12 -0
- package/dist/memory/session-digest.js +47 -0
- package/dist/memory/stale.js +2 -1
- package/dist/model-alias.d.ts +18 -2
- package/dist/model-alias.js +27 -3
- package/dist/model-catalog.js +0 -1
- package/dist/model-list.js +2 -1
- package/dist/model-overrides.d.ts +30 -0
- package/dist/model-overrides.js +48 -0
- package/dist/orchestrator/specialists-registry.js +2 -3
- package/dist/orchestrator/specialists.js +4 -4
- package/dist/policy/ca-certs.d.ts +8 -0
- package/dist/policy/ca-certs.js +30 -0
- package/dist/policy/proxy.d.ts +10 -3
- package/dist/policy/proxy.js +33 -6
- package/dist/pricing.js +4 -0
- package/dist/providers.d.ts +7 -0
- package/dist/providers.js +39 -5
- package/dist/semantic/embedder.d.ts +15 -0
- package/dist/semantic/embedder.js +25 -0
- package/dist/semantic/semantic-index.d.ts +10 -2
- package/dist/semantic/semantic-index.js +16 -7
- package/dist/sessions/session-store.js +2 -1
- package/dist/skills/builtin.d.ts +2 -0
- package/dist/skills/builtin.js +68 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/loader.d.ts +10 -3
- package/dist/skills/loader.js +20 -7
- package/dist/skills/marketplace.js +2 -1
- package/dist/skills/remote.d.ts +14 -0
- package/dist/skills/remote.js +131 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/steering/extra-instructions.d.ts +8 -0
- package/dist/steering/extra-instructions.js +104 -0
- package/dist/steering/folder-instructions.d.ts +8 -0
- package/dist/steering/folder-instructions.js +28 -0
- package/dist/steering/select.js +2 -1
- package/dist/timed-fetch.d.ts +12 -0
- package/dist/timed-fetch.js +83 -0
- package/dist/tools/apply-patch.d.ts +52 -0
- package/dist/tools/apply-patch.js +197 -0
- package/dist/tools/ask-user.d.ts +27 -0
- package/dist/tools/ask-user.js +32 -0
- package/dist/tools/browser-tools.d.ts +6 -6
- package/dist/tools/browser-tools.js +3 -13
- package/dist/tools/codebase-search.d.ts +4 -2
- package/dist/tools/codebase-search.js +2 -2
- package/dist/tools/edit-file.js +5 -3
- package/dist/tools/fetch-url.d.ts +7 -0
- package/dist/tools/fetch-url.js +52 -19
- package/dist/tools/find-symbol.d.ts +2 -2
- package/dist/tools/ignore-rules.js +2 -1
- package/dist/tools/index.d.ts +5 -1
- package/dist/tools/index.js +10 -1
- package/dist/tools/memory.d.ts +6 -6
- package/dist/tools/notebook.d.ts +2 -2
- package/dist/tools/permission-rules.d.ts +32 -0
- package/dist/tools/permission-rules.js +182 -0
- package/dist/tools/read-file.d.ts +5 -0
- package/dist/tools/read-file.js +7 -2
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +2 -0
- package/dist/tools/run-command.d.ts +4 -2
- package/dist/tools/run-command.js +12 -8
- package/dist/tools/sandbox-loader.js +3 -13
- package/dist/tools/search-files.d.ts +8 -2
- package/dist/tools/search-files.js +43 -0
- package/dist/tools/shared.d.ts +5 -0
- package/dist/tools/shared.js +30 -2
- package/dist/tools/shell-env.d.ts +7 -0
- package/dist/tools/shell-env.js +9 -0
- package/dist/tools/sub-agent-wait.d.ts +29 -0
- package/dist/tools/sub-agent-wait.js +60 -0
- package/dist/tools/sub-agent.d.ts +50 -4
- package/dist/tools/sub-agent.js +210 -125
- package/dist/tools/text-encoding.d.ts +10 -0
- package/dist/tools/text-encoding.js +40 -0
- package/dist/tools/todo.d.ts +36 -0
- package/dist/tools/todo.js +42 -0
- package/dist/tools/write-file.js +15 -9
- package/dist/tools/write-tools.d.ts +12 -0
- package/dist/tools/write-tools.js +18 -0
- package/dist/types.d.ts +73 -0
- package/dist/worktree/worktree.d.ts +3 -0
- package/dist/worktree/worktree.js +22 -0
- package/package.json +1 -1
package/dist/lsp/client.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The absolute path of `command` found on PATH (with PATHEXT on Windows), or undefined. Never the current directory.
|
|
3
|
+
* A command that already names a path is returned as it is.
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolveOnPath(command: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string | undefined;
|
|
1
6
|
export type LspSeverity = 'error' | 'warning' | 'info' | 'hint';
|
|
2
7
|
export interface LspDiagnostic {
|
|
3
8
|
/** 1-based. */
|
package/dist/lsp/client.js
CHANGED
|
@@ -4,6 +4,37 @@
|
|
|
4
4
|
* JSON-RPC 2.0 with `Content-Length` framing is small enough to own.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
+
import { statSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
/**
|
|
10
|
+
* The absolute path of `command` found on PATH (with PATHEXT on Windows), or undefined. Never the current directory.
|
|
11
|
+
* A command that already names a path is returned as it is.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveOnPath(command, env, platform) {
|
|
14
|
+
if (/[\\/]/.test(command))
|
|
15
|
+
return command;
|
|
16
|
+
const isFile = (p) => {
|
|
17
|
+
try {
|
|
18
|
+
return statSync(p).isFile();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const win = platform === 'win32';
|
|
25
|
+
const dirs = (env.PATH ?? env.Path ?? '').split(win ? ';' : path.delimiter).filter(Boolean);
|
|
26
|
+
const exts = win ? ['', ...(env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean).map((e) => e.toLowerCase())] : [''];
|
|
27
|
+
for (const dir of dirs) {
|
|
28
|
+
for (const ext of exts) {
|
|
29
|
+
if (win && ext === '' && !/\.[a-z0-9]+$/i.test(command))
|
|
30
|
+
continue; // Windows runs nothing without an extension
|
|
31
|
+
const candidate = path.join(dir, command + ext);
|
|
32
|
+
if (isFile(candidate))
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
7
38
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
39
|
/**
|
|
9
40
|
* Servers echo document URIs in their own spelling (`file:///c%3A/x` for the
|
|
@@ -45,9 +76,14 @@ export class LspClient {
|
|
|
45
76
|
* the spawn error (e.g. ENOENT) when the server binary is not installed.
|
|
46
77
|
*/
|
|
47
78
|
static async start(spec, rootDir, timeoutMs) {
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
const
|
|
79
|
+
// The server runs in the workspace, and Windows looks in the current directory before PATH: a repository could
|
|
80
|
+
// ship its own `typescript-language-server.cmd`. So a bare name is resolved on PATH first, never in the workspace.
|
|
81
|
+
const command = resolveOnPath(spec.command, process.env, process.platform);
|
|
82
|
+
if (!command)
|
|
83
|
+
throw Object.assign(new Error(`${spec.command} is not installed (not found on PATH)`), { code: 'ENOENT' });
|
|
84
|
+
// npm `.cmd` shims need a shell on Windows; the path is quoted because it is now absolute and may contain spaces.
|
|
85
|
+
const shell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(command);
|
|
86
|
+
const proc = spawn(shell ? `"${command}"` : command, spec.args ?? [], { cwd: rootDir, stdio: ['pipe', 'pipe', 'pipe'], shell });
|
|
51
87
|
const spawned = new Promise((_, reject) => proc.once('error', reject));
|
|
52
88
|
const client = new LspClient(proc);
|
|
53
89
|
const rootUri = pathToFileURL(rootDir).href;
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export { loadMcpConfig, parseMcpConfig } from './mcp-config.js';
|
|
2
2
|
export type { McpConfigFile, McpServerSpec } from './mcp-config.js';
|
|
3
3
|
export { jsonSchemaToZod } from './json-schema-to-zod.js';
|
|
4
|
-
export { connectAll, connectOne, closeAll, mcpResultToValue } from './mcp-client.js';
|
|
4
|
+
export { connectAll, connectOne, closeAll, mcpInstructionsBlock, mcpResultToValue, McpAuthRequiredError } from './mcp-client.js';
|
|
5
|
+
export type { ConnectOptions } from './mcp-client.js';
|
|
6
|
+
export { forgetSavedAuth, defaultMcpAuthDir } from './oauth.js';
|
|
7
|
+
export type { McpOAuthOptions } from './oauth.js';
|
|
8
|
+
export { createMcpResourceTools } from './resource-tools.js';
|
|
5
9
|
export { BUILTIN_MCP_CATALOG, loadMcpCatalog } from './catalog.js';
|
|
6
10
|
export { McpManager, expandSpec } from './manager.js';
|
|
7
11
|
export type { McpManagerOptions, McpServerState, McpServerStatus } from './manager.js';
|
package/dist/mcp/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { loadMcpConfig, parseMcpConfig } from './mcp-config.js';
|
|
2
2
|
export { jsonSchemaToZod } from './json-schema-to-zod.js';
|
|
3
|
-
export { connectAll, connectOne, closeAll, mcpResultToValue } from './mcp-client.js';
|
|
3
|
+
export { connectAll, connectOne, closeAll, mcpInstructionsBlock, mcpResultToValue, McpAuthRequiredError } from './mcp-client.js';
|
|
4
|
+
export { forgetSavedAuth, defaultMcpAuthDir } from './oauth.js';
|
|
5
|
+
export { createMcpResourceTools } from './resource-tools.js';
|
|
4
6
|
export { BUILTIN_MCP_CATALOG, loadMcpCatalog } from './catalog.js';
|
|
5
7
|
export { McpManager, expandSpec } from './manager.js';
|
|
6
8
|
export { assertValidServerName, registerMcpServer, setMcpServerDisabled } from './registry.js';
|
package/dist/mcp/manager.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ToolDefinition } from '../types.js';
|
|
2
|
-
import { type ConnectedMcpServer } from './mcp-client.js';
|
|
2
|
+
import { type ConnectOptions, type ConnectedMcpServer } from './mcp-client.js';
|
|
3
|
+
import type { McpOAuthOptions } from './oauth.js';
|
|
3
4
|
import type { McpServerSpec } from './mcp-config.js';
|
|
4
5
|
import { type OrgPolicy } from '../policy/policy.js';
|
|
5
|
-
export type McpServerState = 'connected' | 'failed' | 'disabled';
|
|
6
|
+
export type McpServerState = 'connected' | 'failed' | 'disabled' | 'needs-auth';
|
|
6
7
|
export interface McpServerStatus {
|
|
7
8
|
name: string;
|
|
8
9
|
state: McpServerState;
|
|
@@ -24,8 +25,13 @@ export interface McpManagerOptions {
|
|
|
24
25
|
connectTimeoutMs?: number;
|
|
25
26
|
/** Organization policy for the MCP allowlist. Unset = the managed policy file; null = none. */
|
|
26
27
|
policy?: OrgPolicy | null;
|
|
28
|
+
/**
|
|
29
|
+
* OAuth sign-in for HTTP servers that want it. Without it such a server is simply "failed" (401). With it, saved
|
|
30
|
+
* tokens are used and a server without them is listed as "needs-auth" until {@link McpManager.signIn} runs.
|
|
31
|
+
*/
|
|
32
|
+
oauth?: Omit<McpOAuthOptions, 'interactive'>;
|
|
27
33
|
/** Test seam. */
|
|
28
|
-
connect?: (name: string, spec: McpServerSpec) => Promise<ConnectedMcpServer>;
|
|
34
|
+
connect?: (name: string, spec: McpServerSpec, options?: ConnectOptions) => Promise<ConnectedMcpServer>;
|
|
29
35
|
}
|
|
30
36
|
/** Replace `${workspaceFolder}` in args, env values, url and headers. */
|
|
31
37
|
export declare function expandSpec(spec: McpServerSpec, cwd: string): McpServerSpec;
|
|
@@ -41,8 +47,16 @@ export declare class McpManager {
|
|
|
41
47
|
ensureConnected(): Promise<void>;
|
|
42
48
|
/** Tools from all connected servers (call {@link ensureConnected} first). */
|
|
43
49
|
tools(): ToolDefinition[];
|
|
50
|
+
/** The servers that are connected right now (call {@link ensureConnected} first). */
|
|
51
|
+
servers(): readonly ConnectedMcpServer[];
|
|
44
52
|
status(): McpServerStatus[];
|
|
45
53
|
/** Drop connections and re-read the config files. */
|
|
54
|
+
private readonly specs;
|
|
55
|
+
/**
|
|
56
|
+
* Sign in to a server listed as `needs-auth`: opens the user's browser, waits for them to finish (up to the sign-in
|
|
57
|
+
* timeout), saves the tokens, then reconnects everything so the server's tools appear.
|
|
58
|
+
*/
|
|
59
|
+
signIn(name: string): Promise<void>;
|
|
46
60
|
reload(): Promise<void>;
|
|
47
61
|
dispose(): Promise<void>;
|
|
48
62
|
private readScope;
|
package/dist/mcp/manager.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* reported in `status()` instead of only written to stderr.
|
|
6
6
|
*/
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { closeAll, connectOne } from './mcp-client.js';
|
|
8
|
+
import { McpAuthRequiredError, closeAll, connectOne } from './mcp-client.js';
|
|
9
9
|
import { loadMcpConfig } from './mcp-config.js';
|
|
10
10
|
import { defaultUserMcpPath } from './marketplace.js';
|
|
11
11
|
import { filterMcpServers, getOrgPolicy } from '../policy/policy.js';
|
|
@@ -50,10 +50,32 @@ export class McpManager {
|
|
|
50
50
|
tools() {
|
|
51
51
|
return this.connected.flatMap((s) => s.tools);
|
|
52
52
|
}
|
|
53
|
+
/** The servers that are connected right now (call {@link ensureConnected} first). */
|
|
54
|
+
servers() {
|
|
55
|
+
return this.connected;
|
|
56
|
+
}
|
|
53
57
|
status() {
|
|
54
58
|
return [...this.statuses];
|
|
55
59
|
}
|
|
56
60
|
/** Drop connections and re-read the config files. */
|
|
61
|
+
specs = new Map();
|
|
62
|
+
/**
|
|
63
|
+
* Sign in to a server listed as `needs-auth`: opens the user's browser, waits for them to finish (up to the sign-in
|
|
64
|
+
* timeout), saves the tokens, then reconnects everything so the server's tools appear.
|
|
65
|
+
*/
|
|
66
|
+
async signIn(name) {
|
|
67
|
+
const oauth = this.options.oauth;
|
|
68
|
+
const spec = this.specs.get(name);
|
|
69
|
+
if (!oauth)
|
|
70
|
+
throw new Error('sign-in is not available here');
|
|
71
|
+
if (!spec)
|
|
72
|
+
throw new Error(`no MCP server named "${name}"`);
|
|
73
|
+
const connect = this.options.connect ?? connectOne;
|
|
74
|
+
const wait = oauth.signInTimeoutMs ?? 5 * 60_000;
|
|
75
|
+
const server = await withTimeout(connect(name, expandSpec(spec, this.options.cwd), { oauth: { ...oauth, interactive: true } }), wait + (this.options.connectTimeoutMs ?? 30_000), `Signing in to MCP server "${name}"`);
|
|
76
|
+
await closeAll([server]);
|
|
77
|
+
await this.reload();
|
|
78
|
+
}
|
|
57
79
|
async reload() {
|
|
58
80
|
await this.dispose();
|
|
59
81
|
await this.ensureConnected();
|
|
@@ -110,10 +132,13 @@ export class McpManager {
|
|
|
110
132
|
if (spec.disabled)
|
|
111
133
|
return { status: { name, state: 'disabled', scope, toolCount: 0 } };
|
|
112
134
|
try {
|
|
113
|
-
|
|
135
|
+
this.specs.set(name, spec);
|
|
136
|
+
const server = await withTimeout(connect(name, expandSpec(spec, cwd), this.options.oauth ? { oauth: { ...this.options.oauth, interactive: false } } : undefined), timeoutMs, `MCP server "${name}"`);
|
|
114
137
|
return { server, status: { name, state: 'connected', scope, toolCount: server.tools.length } };
|
|
115
138
|
}
|
|
116
139
|
catch (err) {
|
|
140
|
+
if (err instanceof McpAuthRequiredError)
|
|
141
|
+
return { status: { name, state: 'needs-auth', scope, toolCount: 0, error: err.message } };
|
|
117
142
|
return { status: { name, state: 'failed', scope, toolCount: 0, error: err.message } };
|
|
118
143
|
}
|
|
119
144
|
}));
|
|
@@ -55,6 +55,8 @@ export declare function installDirNameFor(source: {
|
|
|
55
55
|
* server" message instead of silently registering nothing.
|
|
56
56
|
*/
|
|
57
57
|
export declare function probeRepoForServer(repoPath: string, subdir?: string): Promise<McpServerSpec>;
|
|
58
|
+
/** `--` ends git's option parsing: a source that starts with `-` (`--upload-pack=…`) must never be read as an option. */
|
|
59
|
+
export declare function gitCloneArgs(url: string, target: string): string[];
|
|
58
60
|
/**
|
|
59
61
|
* Install an MCP server from a catalog entry or a raw GitHub URL.
|
|
60
62
|
* Returns the {@link InstalledMcpServer} describing what was installed.
|
package/dist/mcp/marketplace.js
CHANGED
|
@@ -27,6 +27,8 @@ import { homedir } from 'node:os';
|
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import { promisify } from 'node:util';
|
|
29
29
|
import { parseMcpConfig } from './mcp-config.js';
|
|
30
|
+
import { isOutsideRel } from '../tools/shared.js';
|
|
31
|
+
import { updateJsonFile } from '../config-file.js';
|
|
30
32
|
const execFileP = promisify(execFile);
|
|
31
33
|
/** Path to the cache root (caller-controlled so tests can inject a temp dir). */
|
|
32
34
|
export function defaultCacheRoot() {
|
|
@@ -152,10 +154,14 @@ export async function probeRepoForServer(repoPath, subdir) {
|
|
|
152
154
|
}
|
|
153
155
|
/** Side-effect: clone the source into the cache dir. */
|
|
154
156
|
async function gitClone(url, target) {
|
|
155
|
-
await execFileP('git',
|
|
157
|
+
await execFileP('git', gitCloneArgs(url, target), {
|
|
156
158
|
windowsHide: true,
|
|
157
159
|
});
|
|
158
160
|
}
|
|
161
|
+
/** `--` ends git's option parsing: a source that starts with `-` (`--upload-pack=…`) must never be read as an option. */
|
|
162
|
+
export function gitCloneArgs(url, target) {
|
|
163
|
+
return ['clone', '--depth', '1', '--quiet', '--', url, target];
|
|
164
|
+
}
|
|
159
165
|
/**
|
|
160
166
|
* Install an MCP server from a catalog entry or a raw GitHub URL.
|
|
161
167
|
* Returns the {@link InstalledMcpServer} describing what was installed.
|
|
@@ -197,7 +203,7 @@ export async function installMcpServer(params) {
|
|
|
197
203
|
// consequences of a bypass (arbitrary recursive delete) are severe.
|
|
198
204
|
const installPath = path.join(cacheRoot, name);
|
|
199
205
|
const relInstall = path.relative(cacheRoot, installPath);
|
|
200
|
-
if (
|
|
206
|
+
if (isOutsideRel(relInstall)) {
|
|
201
207
|
throw new Error(`Refusing to install "${name}": resolved path escapes the cache root.`);
|
|
202
208
|
}
|
|
203
209
|
if (existsSync(installPath)) {
|
|
@@ -260,8 +266,8 @@ export async function readUserMcpRegistry(userMcpPath) {
|
|
|
260
266
|
}
|
|
261
267
|
/** Write the user's MCP registry, creating parent directories as needed. */
|
|
262
268
|
export async function writeUserMcpRegistry(userMcpPath, config) {
|
|
263
|
-
|
|
264
|
-
await
|
|
269
|
+
// Atomic, and one writer at a time (see updateJsonFile).
|
|
270
|
+
await updateJsonFile(userMcpPath, () => config);
|
|
265
271
|
}
|
|
266
272
|
/** Remove an installed MCP server by name. Idempotent: succeeds even when absent. */
|
|
267
273
|
export async function uninstallMcpServer(params) {
|
package/dist/mcp/mcp-client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { type McpOAuthOptions } from './oauth.js';
|
|
2
3
|
import type { McpConfigFile, McpServerSpec } from './mcp-config.js';
|
|
3
4
|
import type { ToolDefinition } from '../types.js';
|
|
4
5
|
/**
|
|
@@ -9,11 +10,20 @@ export interface ConnectedMcpServer {
|
|
|
9
10
|
name: string;
|
|
10
11
|
client: Client;
|
|
11
12
|
tools: ToolDefinition[];
|
|
13
|
+
/** What the server says about using it (from `initialize`); shown to the model next to its tools. */
|
|
14
|
+
instructions?: string | undefined;
|
|
12
15
|
}
|
|
13
16
|
/** Connect to every server in a parsed MCP config. Failed connections are skipped with a warning. */
|
|
14
|
-
export declare function connectAll(config: McpConfigFile): Promise<ConnectedMcpServer[]>;
|
|
17
|
+
export declare function connectAll(config: McpConfigFile, options?: ConnectOptions): Promise<ConnectedMcpServer[]>;
|
|
18
|
+
export { McpAuthRequiredError } from './oauth.js';
|
|
19
|
+
export interface ConnectOptions {
|
|
20
|
+
/** Let an HTTP server that wants OAuth sign-in use saved tokens, and (with `interactive`) ask the user to sign in. */
|
|
21
|
+
oauth?: McpOAuthOptions;
|
|
22
|
+
}
|
|
15
23
|
/** Connect to a single server and pull its tools. */
|
|
16
|
-
export declare function connectOne(name: string, spec: McpServerSpec): Promise<ConnectedMcpServer>;
|
|
24
|
+
export declare function connectOne(name: string, spec: McpServerSpec, options?: ConnectOptions): Promise<ConnectedMcpServer>;
|
|
25
|
+
/** The instructions servers sent with `initialize`, as one prompt block (empty when none sent any). */
|
|
26
|
+
export declare function mcpInstructionsBlock(servers: readonly ConnectedMcpServer[]): string;
|
|
17
27
|
/** Close all clients and their underlying transports. */
|
|
18
28
|
export declare function closeAll(servers: ConnectedMcpServer[]): Promise<void>;
|
|
19
29
|
/**
|
package/dist/mcp/mcp-client.js
CHANGED
|
@@ -24,12 +24,14 @@
|
|
|
24
24
|
import { readFileSync } from 'node:fs';
|
|
25
25
|
import { dirname, join } from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
27
28
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
28
29
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
29
30
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
30
31
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
31
32
|
import { filterMcpServers, getOrgPolicy } from '../policy/policy.js';
|
|
32
33
|
import { jsonSchemaToZod } from './json-schema-to-zod.js';
|
|
34
|
+
import { ForgeOAuthProvider, McpAuthRequiredError, defaultMcpAuthDir, readSavedAuth } from './oauth.js';
|
|
33
35
|
/**
|
|
34
36
|
* Self-identifying info we send to every MCP server during the `initialize`
|
|
35
37
|
* handshake. Version is read from our own package.json so it stays in sync
|
|
@@ -53,7 +55,7 @@ function readPackageVersion() {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
/** Connect to every server in a parsed MCP config. Failed connections are skipped with a warning. */
|
|
56
|
-
export async function connectAll(config) {
|
|
58
|
+
export async function connectAll(config, options = {}) {
|
|
57
59
|
const { allowed, blocked } = filterMcpServers(config.mcpServers, getOrgPolicy()?.policy);
|
|
58
60
|
for (const name of blocked) {
|
|
59
61
|
process.stderr.write(`forge: MCP server "${name}" is blocked by organization policy
|
|
@@ -65,10 +67,14 @@ export async function connectAll(config) {
|
|
|
65
67
|
if (spec.disabled)
|
|
66
68
|
continue;
|
|
67
69
|
try {
|
|
68
|
-
const server = await connectOne(name, spec);
|
|
70
|
+
const server = await connectOne(name, spec, options);
|
|
69
71
|
results.push(server);
|
|
70
72
|
}
|
|
71
73
|
catch (err) {
|
|
74
|
+
if (err instanceof McpAuthRequiredError) {
|
|
75
|
+
process.stderr.write(`forge: MCP server "${name}" needs you to sign in. Run: forge mcp login ${name}\n`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
72
78
|
// Skip failed servers so one bad entry doesn't take down the whole
|
|
73
79
|
// session; surface the error so the caller can log it.
|
|
74
80
|
process.stderr.write(`forge: MCP server "${name}" failed to connect: ${err.message}\n`);
|
|
@@ -76,21 +82,68 @@ export async function connectAll(config) {
|
|
|
76
82
|
}
|
|
77
83
|
return results;
|
|
78
84
|
}
|
|
85
|
+
export { McpAuthRequiredError } from './oauth.js';
|
|
86
|
+
/** A 401 from a server we connected to without credentials. */
|
|
87
|
+
function isUnauthorized(err) {
|
|
88
|
+
const e = err;
|
|
89
|
+
return err instanceof UnauthorizedError || e?.code === 401 || (typeof e?.message === 'string' && /\b401\b/.test(e.message));
|
|
90
|
+
}
|
|
79
91
|
/** Connect to a single server and pull its tools. */
|
|
80
|
-
export async function connectOne(name, spec) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
92
|
+
export async function connectOne(name, spec, options = {}) {
|
|
93
|
+
const timeout = spec.timeout ? { timeout: spec.timeout } : undefined;
|
|
94
|
+
let client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
95
|
+
const oauth = options.oauth;
|
|
96
|
+
// A static Authorization header is the user's own choice of credentials: leave OAuth out of it.
|
|
97
|
+
const hasStaticAuth = Object.keys(spec.headers ?? {}).some((h) => h.toLowerCase() === 'authorization');
|
|
98
|
+
const wantsOAuth = Boolean(oauth && spec.url && !spec.command && !hasStaticAuth);
|
|
99
|
+
if (!wantsOAuth) {
|
|
100
|
+
await client.connect(buildTransport(spec), timeout);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const url = spec.url;
|
|
104
|
+
const haveTokens = Boolean((await readSavedAuth(oauth.storeDir ?? defaultMcpAuthDir(), url))?.tokens);
|
|
105
|
+
let provider;
|
|
106
|
+
if (haveTokens || oauth.interactive) {
|
|
107
|
+
provider = await ForgeOAuthProvider.create(name, url, oauth);
|
|
108
|
+
}
|
|
109
|
+
let transport = buildTransport(spec, provider);
|
|
110
|
+
try {
|
|
111
|
+
await client.connect(transport, timeout);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (!provider) {
|
|
115
|
+
// Tried without credentials and the server refused: it needs a sign-in the user has not done yet.
|
|
116
|
+
if (isUnauthorized(err))
|
|
117
|
+
throw new McpAuthRequiredError(name);
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
if (!(err instanceof UnauthorizedError))
|
|
121
|
+
throw err;
|
|
122
|
+
// The SDK started the sign-in (the browser is open); wait for the user, then connect again with the tokens.
|
|
123
|
+
try {
|
|
124
|
+
const code = await provider.waitForCode();
|
|
125
|
+
await transport.finishAuth(code);
|
|
126
|
+
}
|
|
127
|
+
catch (signInErr) {
|
|
128
|
+
provider.cancel();
|
|
129
|
+
throw signInErr;
|
|
130
|
+
}
|
|
131
|
+
await client.close().catch(() => undefined);
|
|
132
|
+
client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
133
|
+
transport = buildTransport(spec, provider);
|
|
134
|
+
await client.connect(transport, timeout);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const { tools: mcpTools } = await client.listTools(undefined, spec.timeout ? { timeout: spec.timeout } : undefined);
|
|
138
|
+
const tools = mcpTools.map((t) => mcpToolToForgeTool(name, t, client, spec.timeout));
|
|
139
|
+
return { name, client, tools, instructions: client.getInstructions()?.trim() || undefined };
|
|
87
140
|
}
|
|
88
141
|
/**
|
|
89
142
|
* Pick the right SDK transport for the server spec. The transport type is
|
|
90
143
|
* either explicit (`type: 'sse'` etc.) or inferred from the shape of the
|
|
91
144
|
* spec: `command` present ⇒ stdio, `url` present ⇒ SSE.
|
|
92
145
|
*/
|
|
93
|
-
function buildTransport(spec) {
|
|
146
|
+
function buildTransport(spec, authProvider) {
|
|
94
147
|
const explicit = spec.type;
|
|
95
148
|
const isStdio = explicit === 'stdio' || (!explicit && Boolean(spec.command));
|
|
96
149
|
const isSse = explicit === 'sse' || (!explicit && Boolean(spec.url) && !explicit);
|
|
@@ -118,10 +171,17 @@ function buildTransport(spec) {
|
|
|
118
171
|
const headers = spec.headers ?? {};
|
|
119
172
|
const requestInit = headers ? { headers } : {};
|
|
120
173
|
if (explicit === 'streamable-http') {
|
|
121
|
-
return new StreamableHTTPClientTransport(new URL(spec.url), { requestInit });
|
|
174
|
+
return new StreamableHTTPClientTransport(new URL(spec.url), { requestInit, ...(authProvider ? { authProvider } : {}) });
|
|
122
175
|
}
|
|
123
176
|
// Default for HTTP-transport specs: SSE (preserves prior behavior).
|
|
124
|
-
return new SSEClientTransport(new URL(spec.url), { requestInit });
|
|
177
|
+
return new SSEClientTransport(new URL(spec.url), { requestInit, ...(authProvider ? { authProvider } : {}) });
|
|
178
|
+
}
|
|
179
|
+
/** The instructions servers sent with `initialize`, as one prompt block (empty when none sent any). */
|
|
180
|
+
export function mcpInstructionsBlock(servers) {
|
|
181
|
+
return servers
|
|
182
|
+
.filter((s) => s.instructions)
|
|
183
|
+
.map((s) => `<mcp-server-instructions server="${s.name.replace(/"/g, '')}">\n${s.instructions}\n</mcp-server-instructions>`)
|
|
184
|
+
.join('\n\n');
|
|
125
185
|
}
|
|
126
186
|
/** Close all clients and their underlying transports. */
|
|
127
187
|
export async function closeAll(servers) {
|
|
@@ -133,7 +193,7 @@ export async function closeAll(servers) {
|
|
|
133
193
|
* namespace of the built-in tools and tells the model which server a tool
|
|
134
194
|
* came from at a glance.
|
|
135
195
|
*/
|
|
136
|
-
function mcpToolToForgeTool(serverName, mcpTool, client) {
|
|
196
|
+
function mcpToolToForgeTool(serverName, mcpTool, client, timeout) {
|
|
137
197
|
const fullName = `mcp__${serverName}__${mcpTool.name}`;
|
|
138
198
|
const description = [
|
|
139
199
|
mcpTool.description ?? '',
|
|
@@ -150,10 +210,7 @@ function mcpToolToForgeTool(serverName, mcpTool, client) {
|
|
|
150
210
|
// destructive so the user gets a confirmation prompt unless --auto.
|
|
151
211
|
destructive: true,
|
|
152
212
|
execute: async (args) => {
|
|
153
|
-
const result = await client.callTool({
|
|
154
|
-
name: mcpTool.name,
|
|
155
|
-
arguments: args,
|
|
156
|
-
});
|
|
213
|
+
const result = await client.callTool({ name: mcpTool.name, arguments: args }, undefined, timeout ? { timeout } : undefined);
|
|
157
214
|
return mcpResultToValue(result);
|
|
158
215
|
},
|
|
159
216
|
};
|
package/dist/mcp/mcp-config.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface McpServerSpec {
|
|
|
45
45
|
headers?: Record<string, string>;
|
|
46
46
|
/** Keep the entry but do not connect it. */
|
|
47
47
|
disabled?: boolean;
|
|
48
|
+
/** Milliseconds to wait for a connection or a tool call before giving up (default: the SDK's 60 s). */
|
|
49
|
+
timeout?: number;
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
50
52
|
* Load and validate a `forge.mcp.json` file. Throws if the file is missing,
|
package/dist/mcp/mcp-config.js
CHANGED
|
@@ -9,6 +9,7 @@ const serverSchema = z
|
|
|
9
9
|
url: z.string().optional(),
|
|
10
10
|
headers: z.record(z.string(), z.string()).optional(),
|
|
11
11
|
disabled: z.boolean().optional(),
|
|
12
|
+
timeout: z.number().int().positive().max(3_600_000).optional(),
|
|
12
13
|
})
|
|
13
14
|
.refine((s) => Boolean(s.command) || Boolean(s.url), {
|
|
14
15
|
message: 'Each MCP server must have either `command` (stdio) or `url` (sse/streamable-http).',
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
2
|
+
import type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
3
|
+
export interface McpOAuthOptions {
|
|
4
|
+
/** Defaults to `~/.forge/mcp-auth`. */
|
|
5
|
+
storeDir?: string;
|
|
6
|
+
/** Show the authorization page to the user (their browser). Throw to refuse. */
|
|
7
|
+
openUrl: (url: string) => void | Promise<void>;
|
|
8
|
+
/** True: may open the browser and wait for the user. False: only use tokens already saved. */
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
/** How long to wait for the user to finish in the browser. Default five minutes. */
|
|
11
|
+
signInTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The server needs the user to sign in, and this connection was not allowed to ask. */
|
|
14
|
+
export declare class McpAuthRequiredError extends Error {
|
|
15
|
+
readonly server: string;
|
|
16
|
+
constructor(server: string);
|
|
17
|
+
}
|
|
18
|
+
interface SavedAuth {
|
|
19
|
+
url: string;
|
|
20
|
+
port?: number;
|
|
21
|
+
clientInformation?: OAuthClientInformationMixed;
|
|
22
|
+
tokens?: OAuthTokens;
|
|
23
|
+
}
|
|
24
|
+
export declare function defaultMcpAuthDir(): string;
|
|
25
|
+
export declare function readSavedAuth(dir: string, serverUrl: string): Promise<SavedAuth | undefined>;
|
|
26
|
+
export declare function forgetSavedAuth(dir: string, serverUrl: string): Promise<void>;
|
|
27
|
+
export declare class ForgeOAuthProvider implements OAuthClientProvider {
|
|
28
|
+
readonly serverName: string;
|
|
29
|
+
private readonly serverUrl;
|
|
30
|
+
private readonly dir;
|
|
31
|
+
private readonly port;
|
|
32
|
+
private readonly options;
|
|
33
|
+
private verifier;
|
|
34
|
+
private expectedState;
|
|
35
|
+
private receiver;
|
|
36
|
+
private constructor();
|
|
37
|
+
static create(serverName: string, serverUrl: string, options: McpOAuthOptions): Promise<ForgeOAuthProvider>;
|
|
38
|
+
get redirectUrl(): string;
|
|
39
|
+
get clientMetadata(): OAuthClientMetadata;
|
|
40
|
+
state(): string;
|
|
41
|
+
private saved;
|
|
42
|
+
clientInformation(): Promise<OAuthClientInformationMixed | undefined>;
|
|
43
|
+
saveClientInformation(info: OAuthClientInformationMixed): Promise<void>;
|
|
44
|
+
tokens(): Promise<OAuthTokens | undefined>;
|
|
45
|
+
saveTokens(tokens: OAuthTokens): Promise<void>;
|
|
46
|
+
saveCodeVerifier(v: string): void;
|
|
47
|
+
codeVerifier(): string;
|
|
48
|
+
invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery'): Promise<void>;
|
|
49
|
+
/** Called by the SDK once it has the authorization URL. Without `interactive` this is where the connection stops. */
|
|
50
|
+
redirectToAuthorization(url: URL): Promise<void>;
|
|
51
|
+
/** After the SDK reported the sign-in as started: the code the browser brings back. */
|
|
52
|
+
waitForCode(): Promise<string>;
|
|
53
|
+
cancel(): void;
|
|
54
|
+
}
|
|
55
|
+
export {};
|