@bridge_gpt/mcp-server 0.2.25 → 0.2.26
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/README.md +59 -9
- package/build/agents.generated.js +1 -1
- package/build/bridge-api-urls.js +31 -0
- package/build/commands.generated.js +4 -4
- package/build/conductor-bundle-artifacts.js +802 -0
- package/build/conductor-bundle-cli.js +256 -0
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +148 -1
- package/build/env-flags.js +31 -0
- package/build/index.js +2509 -296
- package/build/init.js +7 -3
- package/build/install-bridge.js +346 -4
- package/build/mcp-host-config.js +521 -0
- package/build/mcp-host-targets.js +194 -0
- package/build/mcp-install-state.js +175 -0
- package/build/pipelines.generated.js +5 -4
- package/build/readme.generated.js +1 -1
- package/build/start-tickets.js +118 -5
- package/build/tool-surface-gating.js +396 -0
- package/build/version.generated.js +1 -1
- package/docs/install/mcp-tool-integrations.md +2 -2
- package/package.json +5 -5
- package/public/js/main.min.js +1 -19
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +4 -4
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-only MCP host-target registry (BAPI-635).
|
|
3
|
+
*
|
|
4
|
+
* Every TypeScript mapping of an AI-coding platform to its MCP config format,
|
|
5
|
+
* path, scope, root key, transport type, write strategy, and vendor CLI lives
|
|
6
|
+
* here as a single additive, data-only registry — mirroring the convention in
|
|
7
|
+
* {@link file:./third-party-mcp-targets.ts}. Downstream consumers (install-bridge,
|
|
8
|
+
* init, mcp-provisioning, doctor, install-doctor) project this registry rather
|
|
9
|
+
* than re-declaring platform path/key literals.
|
|
10
|
+
*
|
|
11
|
+
* This module performs NO I/O. Detection callbacks receive an injected
|
|
12
|
+
* {@link HostDetectionContext} (cwd, homedir, env, exists) so the registry
|
|
13
|
+
* performs no direct filesystem, subprocess, home-directory, or environment
|
|
14
|
+
* reads — every input arrives through the injected context. Adding a new
|
|
15
|
+
* platform is a matter of adding one entry to {@link MCP_HOST_TARGETS} plus its
|
|
16
|
+
* `PlatformId` union member.
|
|
17
|
+
*/
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Path helpers (pure)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/** POSIX-style join that avoids importing node:path (keeps module I/O-free). */
|
|
22
|
+
function joinPath(base, rel) {
|
|
23
|
+
const trimmedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
24
|
+
const trimmedRel = rel.startsWith("/") ? rel.slice(1) : rel;
|
|
25
|
+
return `${trimmedBase}/${trimmedRel}`;
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Registry — additive; add a new entry (and a PlatformId member) per platform.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* The single source of truth for every platform's MCP host mapping. Consumers
|
|
32
|
+
* MUST project this registry rather than re-declaring path/key literals.
|
|
33
|
+
*/
|
|
34
|
+
export const MCP_HOST_TARGETS = {
|
|
35
|
+
"claude-code": {
|
|
36
|
+
id: "claude-code",
|
|
37
|
+
label: "Claude Code",
|
|
38
|
+
scope: "project",
|
|
39
|
+
relPath: ".mcp.json",
|
|
40
|
+
displayPath: ".mcp.json",
|
|
41
|
+
format: "json",
|
|
42
|
+
topLevelKey: "mcpServers",
|
|
43
|
+
transportType: undefined, // Claude Code omits `type`.
|
|
44
|
+
vendorCli: { bin: "claude", kind: "claude-add-json" },
|
|
45
|
+
worktreeSupported: true,
|
|
46
|
+
writeStrategy: "vendor-first",
|
|
47
|
+
// Claude Code is always a candidate default.
|
|
48
|
+
detect: () => true,
|
|
49
|
+
},
|
|
50
|
+
cursor: {
|
|
51
|
+
id: "cursor",
|
|
52
|
+
label: "Cursor",
|
|
53
|
+
scope: "project",
|
|
54
|
+
relPath: ".cursor/mcp.json",
|
|
55
|
+
displayPath: ".cursor/mcp.json",
|
|
56
|
+
format: "json",
|
|
57
|
+
topLevelKey: "mcpServers",
|
|
58
|
+
transportType: "stdio",
|
|
59
|
+
worktreeSupported: true,
|
|
60
|
+
writeStrategy: "direct",
|
|
61
|
+
detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".cursor")) ||
|
|
62
|
+
typeof ctx.env.CURSOR_TRACE_DIR === "string" &&
|
|
63
|
+
ctx.env.CURSOR_TRACE_DIR.length > 0,
|
|
64
|
+
},
|
|
65
|
+
"copilot-vscode": {
|
|
66
|
+
id: "copilot-vscode",
|
|
67
|
+
label: "GitHub Copilot (VS Code)",
|
|
68
|
+
scope: "project",
|
|
69
|
+
relPath: ".vscode/mcp.json",
|
|
70
|
+
displayPath: ".vscode/mcp.json",
|
|
71
|
+
format: "json",
|
|
72
|
+
topLevelKey: "servers",
|
|
73
|
+
transportType: "stdio",
|
|
74
|
+
worktreeSupported: false,
|
|
75
|
+
writeStrategy: "direct",
|
|
76
|
+
detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".vscode")),
|
|
77
|
+
},
|
|
78
|
+
"copilot-cli": {
|
|
79
|
+
id: "copilot-cli",
|
|
80
|
+
label: "GitHub Copilot CLI",
|
|
81
|
+
scope: "global",
|
|
82
|
+
absPathResolver: (homedir) => joinPath(homedir, ".copilot/mcp-config.json"),
|
|
83
|
+
displayPath: "~/.copilot/mcp-config.json",
|
|
84
|
+
format: "json",
|
|
85
|
+
topLevelKey: "mcpServers",
|
|
86
|
+
transportType: "local",
|
|
87
|
+
extraEntryKeys: { tools: ["*"] },
|
|
88
|
+
vendorCli: { bin: "copilot", kind: "copilot-add" },
|
|
89
|
+
worktreeSupported: false,
|
|
90
|
+
writeStrategy: "vendor-first",
|
|
91
|
+
// No existing probe supports Copilot CLI; never a default.
|
|
92
|
+
detect: () => false,
|
|
93
|
+
},
|
|
94
|
+
codex: {
|
|
95
|
+
id: "codex",
|
|
96
|
+
label: "OpenAI Codex",
|
|
97
|
+
scope: "global",
|
|
98
|
+
absPathResolver: (homedir) => joinPath(homedir, ".codex/config.toml"),
|
|
99
|
+
displayPath: "~/.codex/config.toml",
|
|
100
|
+
format: "toml",
|
|
101
|
+
topLevelKey: "mcp_servers",
|
|
102
|
+
transportType: undefined, // TOML table shape has no `type`.
|
|
103
|
+
vendorCli: { bin: "codex", kind: "codex-add" },
|
|
104
|
+
worktreeSupported: false,
|
|
105
|
+
writeStrategy: "vendor-first",
|
|
106
|
+
detect: (ctx) => ctx.exists(joinPath(ctx.homedir, ".codex")),
|
|
107
|
+
},
|
|
108
|
+
windsurf: {
|
|
109
|
+
id: "windsurf",
|
|
110
|
+
label: "Windsurf",
|
|
111
|
+
scope: "global",
|
|
112
|
+
absPathResolver: (homedir) => joinPath(homedir, ".codeium/windsurf/mcp_config.json"),
|
|
113
|
+
displayPath: "~/.codeium/windsurf/mcp_config.json",
|
|
114
|
+
format: "json",
|
|
115
|
+
topLevelKey: "mcpServers",
|
|
116
|
+
// Windsurf historically renders with no `type` (like Claude); preserve that.
|
|
117
|
+
transportType: undefined,
|
|
118
|
+
worktreeSupported: false,
|
|
119
|
+
// Preserve current behavior: never auto-modify the global Windsurf file.
|
|
120
|
+
writeStrategy: "manual-instructions",
|
|
121
|
+
detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".windsurf")) ||
|
|
122
|
+
ctx.exists(joinPath(ctx.cwd, ".windsurfrules")),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
/** Stable registry order for deterministic prompts, previews, and reports. */
|
|
126
|
+
export const HOST_PLATFORM_ORDER = [
|
|
127
|
+
"claude-code",
|
|
128
|
+
"cursor",
|
|
129
|
+
"copilot-vscode",
|
|
130
|
+
"copilot-cli",
|
|
131
|
+
"codex",
|
|
132
|
+
"windsurf",
|
|
133
|
+
];
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Projections & lookups (pure)
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
/** All registry definitions in stable {@link HOST_PLATFORM_ORDER}. */
|
|
138
|
+
export function allHostTargets() {
|
|
139
|
+
return HOST_PLATFORM_ORDER.map((id) => MCP_HOST_TARGETS[id]);
|
|
140
|
+
}
|
|
141
|
+
/** Whether a string is a registered platform ID. */
|
|
142
|
+
export function isHostPlatformId(value) {
|
|
143
|
+
return Object.prototype.hasOwnProperty.call(MCP_HOST_TARGETS, value);
|
|
144
|
+
}
|
|
145
|
+
/** Look up a definition by ID, or `undefined`. */
|
|
146
|
+
export function getHostTarget(id) {
|
|
147
|
+
return isHostPlatformId(id) ? MCP_HOST_TARGETS[id] : undefined;
|
|
148
|
+
}
|
|
149
|
+
/** The two worktree-supported targets, in stable order (Claude, Cursor). */
|
|
150
|
+
export function getWorktreeHostTargets() {
|
|
151
|
+
return allHostTargets().filter((t) => t.worktreeSupported);
|
|
152
|
+
}
|
|
153
|
+
/** Project-scoped JSON targets (used by init, launcher-token merge, doctor). */
|
|
154
|
+
export function getProjectJsonTargets() {
|
|
155
|
+
return allHostTargets().filter((t) => t.scope === "project" && t.format === "json");
|
|
156
|
+
}
|
|
157
|
+
/** Targets written automatically (vendor-first or direct). */
|
|
158
|
+
export function getAutomaticHostTargets() {
|
|
159
|
+
return allHostTargets().filter((t) => t.writeStrategy !== "manual-instructions");
|
|
160
|
+
}
|
|
161
|
+
/** Targets that only ever emit manual instructions. */
|
|
162
|
+
export function getManualHostTargets() {
|
|
163
|
+
return allHostTargets().filter((t) => t.writeStrategy === "manual-instructions");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the absolute filesystem path for a target from an injected context.
|
|
167
|
+
* Project targets resolve below `ctx.cwd`; global targets below `ctx.homedir`.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveHostTargetPath(target, ctx) {
|
|
170
|
+
if (target.scope === "project") {
|
|
171
|
+
if (!target.relPath) {
|
|
172
|
+
throw new Error(`project target ${target.id} is missing relPath`);
|
|
173
|
+
}
|
|
174
|
+
return joinPath(ctx.cwd, target.relPath);
|
|
175
|
+
}
|
|
176
|
+
if (!target.absPathResolver) {
|
|
177
|
+
throw new Error(`global target ${target.id} is missing absPathResolver`);
|
|
178
|
+
}
|
|
179
|
+
return target.absPathResolver(ctx.homedir);
|
|
180
|
+
}
|
|
181
|
+
/** The user-facing display path for a target (never an absolute home path). */
|
|
182
|
+
export function displayPathForTarget(target) {
|
|
183
|
+
return target.displayPath;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Run detection across every registry entry and return the set of platform IDs
|
|
187
|
+
* whose `detect` callback is true — the seed for interactive multi-select
|
|
188
|
+
* defaults.
|
|
189
|
+
*/
|
|
190
|
+
export function detectDefaultPlatforms(ctx) {
|
|
191
|
+
return allHostTargets()
|
|
192
|
+
.filter((t) => t.detect(ctx))
|
|
193
|
+
.map((t) => t.id);
|
|
194
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-developer MCP install-state persistence (BAPI-635).
|
|
3
|
+
*
|
|
4
|
+
* Records which host platforms a developer selected during `install-bridge`, so
|
|
5
|
+
* the install skill and `doctor` can report selected-vs-present configuration
|
|
6
|
+
* without re-detecting. The state file `.bridge/install-state.json` is
|
|
7
|
+
* per-developer and is gitignored before it is written.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY: the state is strictly SECRET-FREE. It stores only a validated
|
|
10
|
+
* platform roster and repo-relative project config paths. It never contains API
|
|
11
|
+
* keys, env blocks, absolute home paths, or vendor command payloads. Reads
|
|
12
|
+
* validate every stored platform ID against {@link MCP_HOST_TARGETS}; writes
|
|
13
|
+
* serialize only the schema-approved fields.
|
|
14
|
+
*
|
|
15
|
+
* All filesystem access is through injected dependencies so the module is fully
|
|
16
|
+
* unit-testable with in-memory fakes.
|
|
17
|
+
*/
|
|
18
|
+
import { HOST_PLATFORM_ORDER, isHostPlatformId, } from "./mcp-host-targets.js";
|
|
19
|
+
/** Current on-disk schema version. */
|
|
20
|
+
export const MCP_INSTALL_STATE_VERSION = 1;
|
|
21
|
+
/** The relative path of the state file within a project. */
|
|
22
|
+
export const MCP_INSTALL_STATE_RELPATH = ".bridge/install-state.json";
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Path helpers (POSIX-normalized; avoids node:path so tests are deterministic).
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
function joinCwd(cwd, rel) {
|
|
27
|
+
const base = cwd.endsWith("/") ? cwd.slice(0, -1) : cwd;
|
|
28
|
+
return `${base}/${rel}`;
|
|
29
|
+
}
|
|
30
|
+
/** Absolute path of the install-state file for a project. */
|
|
31
|
+
export function installStatePath(cwd) {
|
|
32
|
+
return joinCwd(cwd, MCP_INSTALL_STATE_RELPATH);
|
|
33
|
+
}
|
|
34
|
+
/** Absolute path of the atomic temp file used during writes. */
|
|
35
|
+
function installStateTempPath(cwd) {
|
|
36
|
+
return joinCwd(cwd, `${MCP_INSTALL_STATE_RELPATH}.tmp`);
|
|
37
|
+
}
|
|
38
|
+
function bridgeDirPath(cwd) {
|
|
39
|
+
return joinCwd(cwd, ".bridge");
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Normalization
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/** Dedupe + order platform IDs by the stable registry order. */
|
|
45
|
+
function normalizePlatforms(ids) {
|
|
46
|
+
const wanted = new Set(ids);
|
|
47
|
+
return HOST_PLATFORM_ORDER.filter((id) => wanted.has(id));
|
|
48
|
+
}
|
|
49
|
+
/** Dedupe + stably order relative project config paths. */
|
|
50
|
+
function normalizeProjectPaths(paths) {
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const p of paths) {
|
|
54
|
+
if (typeof p === "string" && p.length > 0 && !seen.has(p)) {
|
|
55
|
+
seen.add(p);
|
|
56
|
+
out.push(p);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
out.sort();
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Read
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
function isEnoent(err) {
|
|
66
|
+
return (typeof err === "object" &&
|
|
67
|
+
err !== null &&
|
|
68
|
+
err.code === "ENOENT");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read and validate the install-state file. Returns a structured outcome; never
|
|
72
|
+
* throws for the ordinary missing/malformed/invalid cases and never resolves an
|
|
73
|
+
* arbitrary path from untrusted platform IDs.
|
|
74
|
+
*/
|
|
75
|
+
export async function readMcpInstallState(cwd, deps) {
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = await deps.readFile(installStatePath(cwd));
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (isEnoent(err))
|
|
82
|
+
return { status: "missing" };
|
|
83
|
+
// A non-ENOENT read error is treated as missing state, not a crash.
|
|
84
|
+
return { status: "missing" };
|
|
85
|
+
}
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { status: "malformed" };
|
|
92
|
+
}
|
|
93
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
94
|
+
return { status: "malformed" };
|
|
95
|
+
}
|
|
96
|
+
const doc = parsed;
|
|
97
|
+
if (doc.version !== MCP_INSTALL_STATE_VERSION) {
|
|
98
|
+
return { status: "unsupported-version", version: doc.version };
|
|
99
|
+
}
|
|
100
|
+
const rawPlatforms = doc.selectedPlatforms;
|
|
101
|
+
if (!Array.isArray(rawPlatforms)) {
|
|
102
|
+
return { status: "invalid", reason: "selectedPlatforms is not an array" };
|
|
103
|
+
}
|
|
104
|
+
for (const id of rawPlatforms) {
|
|
105
|
+
if (typeof id !== "string" || !isHostPlatformId(id)) {
|
|
106
|
+
return {
|
|
107
|
+
status: "invalid",
|
|
108
|
+
reason: `unknown platform id in selectedPlatforms`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const rawPaths = doc.projectConfigPaths;
|
|
113
|
+
if (rawPaths !== undefined && !Array.isArray(rawPaths)) {
|
|
114
|
+
return { status: "invalid", reason: "projectConfigPaths is not an array" };
|
|
115
|
+
}
|
|
116
|
+
const projectConfigPaths = Array.isArray(rawPaths)
|
|
117
|
+
? rawPaths.filter((p) => typeof p === "string")
|
|
118
|
+
: [];
|
|
119
|
+
return {
|
|
120
|
+
status: "valid",
|
|
121
|
+
state: {
|
|
122
|
+
version: MCP_INSTALL_STATE_VERSION,
|
|
123
|
+
selectedPlatforms: normalizePlatforms(rawPlatforms),
|
|
124
|
+
projectConfigPaths: normalizeProjectPaths(projectConfigPaths),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Write
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
/** Serialize an install-state document deterministically (stable key order). */
|
|
132
|
+
export function serializeMcpInstallState(state) {
|
|
133
|
+
// Explicit key order — do not rely on object insertion order for stability.
|
|
134
|
+
const ordered = {
|
|
135
|
+
version: state.version,
|
|
136
|
+
selectedPlatforms: state.selectedPlatforms,
|
|
137
|
+
projectConfigPaths: state.projectConfigPaths,
|
|
138
|
+
};
|
|
139
|
+
return JSON.stringify(ordered, null, 2) + "\n";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Write the install-state file deterministically and atomically (temp file then
|
|
143
|
+
* rename). Only schema-approved fields are serialized; any extra input data is
|
|
144
|
+
* ignored, so no secret, env block, absolute home path, or vendor payload can
|
|
145
|
+
* reach the file. On failure, attempts best-effort temp cleanup and returns a
|
|
146
|
+
* secret-free error without reporting success.
|
|
147
|
+
*/
|
|
148
|
+
export async function writeMcpInstallState(cwd, input, deps) {
|
|
149
|
+
const state = {
|
|
150
|
+
version: MCP_INSTALL_STATE_VERSION,
|
|
151
|
+
selectedPlatforms: normalizePlatforms(input.selectedPlatforms),
|
|
152
|
+
projectConfigPaths: normalizeProjectPaths(input.projectConfigPaths),
|
|
153
|
+
};
|
|
154
|
+
const finalPath = installStatePath(cwd);
|
|
155
|
+
const tempPath = installStateTempPath(cwd);
|
|
156
|
+
const serialized = serializeMcpInstallState(state);
|
|
157
|
+
try {
|
|
158
|
+
await deps.mkdir(bridgeDirPath(cwd), { recursive: true });
|
|
159
|
+
await deps.writeFile(tempPath, serialized);
|
|
160
|
+
await deps.rename(tempPath, finalPath);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (deps.unlink) {
|
|
164
|
+
try {
|
|
165
|
+
await deps.unlink(tempPath);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Best-effort cleanup only; ignore secondary failures.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
172
|
+
return { ok: false, error: `failed to persist install state: ${message}` };
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, path: finalPath, state };
|
|
175
|
+
}
|