@mushi-mushi/mcp 0.16.2 → 0.17.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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * FILE: packages/mcp/src/clients.ts
3
+ * PURPOSE: Shared, pure (React-free) client registry + install-config builders for
4
+ * every supported AI agent / IDE client.
5
+ *
6
+ * OVERVIEW:
7
+ * - Exports `MCP_CLIENTS: McpClientDef[]` — one entry per supported client.
8
+ * - Each entry carries metadata (id, label, method, transport) and a pure `build()`
9
+ * function that returns the install artifact (deeplink URL, JSON config, or CLI command).
10
+ * - Both the admin console ConnectStudio and the public docs /connect landing consume
11
+ * this module — the single source of truth so the two surfaces never drift.
12
+ *
13
+ * DEPENDENCIES:
14
+ * - feature-groups.ts (DEFAULT_FEATURE_GROUPS, featuresQueryString, appendFeaturesToUrl)
15
+ * - branding.ts (MUSHI_ICON_PNG_URL)
16
+ *
17
+ * USAGE:
18
+ * import { MCP_CLIENTS } from '@mushi-mushi/mcp/clients'
19
+ * const cursor = MCP_CLIENTS.find(c => c.id === 'cursor')!
20
+ * const result = cursor.build({ projectId, projectName, apiKey, endpoint, mcpHttpUrl })
21
+ * // result.kind === 'deeplink' → window.open(result.url)
22
+ *
23
+ * CLIENT INSTALL MATRIX (verified Jun 2026):
24
+ * - cursor / vscode / vscode-insiders — one-click deeplink
25
+ * - windsurf — copy-config to ~/.codeium/windsurf/mcp_config.json
26
+ * (uses `serverUrl`, NOT `url` — common footgun)
27
+ * - cline — copy-config to cline_mcp_settings.json
28
+ * - claude-code — CLI command: claude mcp add --transport http …
29
+ * - claude-desktop — copy-config to claude_desktop_config.json
30
+ * - zed — copy-config to ~/.config/zed/settings.json
31
+ * - any — hosted Streamable HTTP URL + Bearer key
32
+ *
33
+ * NOTES:
34
+ * - All builders are pure functions — no side-effects, no DOM, no React.
35
+ * - Cursor base64-encodes the config object without a `name` key in the payload.
36
+ * - VS Code includes `type:'stdio'` in the config and `name` as a separate query param.
37
+ * - Windsurf uses `serverUrl` for remote HTTP (NOT `url`) — encoding both would break it.
38
+ * - The `cliIde` field drives `mushi setup --ide <cliIde>` in the CLI lane.
39
+ */
40
+ interface StdioConfig {
41
+ command: string;
42
+ args: string[];
43
+ env: Record<string, string>;
44
+ icon?: string;
45
+ }
46
+ interface HttpConfig {
47
+ type: 'http';
48
+ url: string;
49
+ headers: Record<string, string>;
50
+ icon?: string;
51
+ }
52
+ type McpClientId = 'cursor' | 'vscode' | 'vscode-insiders' | 'windsurf' | 'cline' | 'claude-code' | 'claude-desktop' | 'zed' | 'any';
53
+ /** The install method determines which UI treatment the console/docs page renders. */
54
+ type McpInstallMethod = 'deeplink' | 'config-json' | 'cli-command' | 'remote-url';
55
+ interface McpBuildInput {
56
+ /** Mushi project UUID — optional when building org-level configs. */
57
+ projectId?: string;
58
+ projectName: string;
59
+ apiKey: string;
60
+ /** Supabase edge function base URL (`MUSHI_API_ENDPOINT`). */
61
+ endpoint: string;
62
+ /** Hosted Streamable HTTP MCP URL (`RESOLVED_MCP_HTTP_URL`). */
63
+ mcpHttpUrl: string;
64
+ }
65
+ type McpBuildResult = {
66
+ kind: 'deeplink';
67
+ url: string;
68
+ } | {
69
+ kind: 'config';
70
+ filePath: string;
71
+ json: string;
72
+ } | {
73
+ kind: 'command';
74
+ text: string;
75
+ } | {
76
+ kind: 'remote-url';
77
+ url: string;
78
+ headerSnippet: string;
79
+ };
80
+ interface McpClientDef {
81
+ id: McpClientId;
82
+ label: string;
83
+ /** Short description shown in the client picker. */
84
+ description: string;
85
+ method: McpInstallMethod;
86
+ transport: 'stdio' | 'http';
87
+ /** Drives `mushi setup --ide <cliIde>` in the CLI lane. */
88
+ cliIde?: 'cursor' | 'claude' | 'continue' | 'zed';
89
+ build(input: McpBuildInput): McpBuildResult;
90
+ }
91
+ /** Build a stable, unique, human-readable MCP server slug for a project. */
92
+ declare function projectServerName(projectId: string, projectName: string): string;
93
+ declare function buildStdioConfigObj(projectId: string | undefined, apiKey: string, apiEndpoint: string, features?: readonly string[]): StdioConfig;
94
+ declare function buildHttpConfigObj(projectId: string | undefined, apiKey: string, mcpHttpUrl: string, features?: readonly string[]): HttpConfig;
95
+ declare const MCP_CLIENTS: McpClientDef[];
96
+ /** Look up a client by id — throws if not found. */
97
+ declare function getMcpClient(id: McpClientId): McpClientDef;
98
+
99
+ export { MCP_CLIENTS, type McpBuildInput, type McpBuildResult, type McpClientDef, type McpClientId, type McpInstallMethod, buildHttpConfigObj as buildRawHttpConfig, buildStdioConfigObj as buildRawStdioConfig, getMcpClient, projectServerName };
@@ -0,0 +1,250 @@
1
+ import {
2
+ MUSHI_ICON_PNG_URL
3
+ } from "./chunk-EHB2BGWX.js";
4
+ import {
5
+ DEFAULT_FEATURE_GROUPS,
6
+ appendFeaturesToUrl,
7
+ featuresQueryString
8
+ } from "./chunk-ZX3XIJ7S.js";
9
+
10
+ // src/clients.ts
11
+ function projectServerName(projectId, projectName) {
12
+ const nameSlug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 22);
13
+ const idSuffix = projectId.replace(/-/g, "").slice(0, 6);
14
+ return `mushi-${nameSlug}-${idSuffix}`;
15
+ }
16
+ function buildStdioConfigObj(projectId, apiKey, apiEndpoint, features = DEFAULT_FEATURE_GROUPS) {
17
+ const env = {
18
+ MUSHI_API_ENDPOINT: apiEndpoint,
19
+ MUSHI_API_KEY: apiKey,
20
+ MUSHI_FEATURES: featuresQueryString(features)
21
+ };
22
+ if (projectId) env.MUSHI_PROJECT_ID = projectId;
23
+ return {
24
+ command: "npx",
25
+ args: ["-y", "@mushi-mushi/mcp@latest"],
26
+ env,
27
+ icon: MUSHI_ICON_PNG_URL
28
+ };
29
+ }
30
+ function buildHttpConfigObj(projectId, apiKey, mcpHttpUrl, features = DEFAULT_FEATURE_GROUPS) {
31
+ const url = appendFeaturesToUrl(mcpHttpUrl, features);
32
+ const headers = {
33
+ Authorization: `Bearer ${apiKey}`,
34
+ "X-Mushi-Api-Key": apiKey
35
+ };
36
+ if (projectId) headers["X-Mushi-Project-Id"] = projectId;
37
+ return { type: "http", url, headers, icon: MUSHI_ICON_PNG_URL };
38
+ }
39
+ function encodeCursorDeeplink(name, config) {
40
+ const encoded = btoa(JSON.stringify(config));
41
+ return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(name)}&config=${encodeURIComponent(encoded)}`;
42
+ }
43
+ function toVsCodeConfig(config) {
44
+ if ("type" in config && config.type === "http") return config;
45
+ return { type: "stdio", ...config };
46
+ }
47
+ function encodeVsCodeDeeplink(scheme, name, config) {
48
+ const withType = toVsCodeConfig(config);
49
+ return `${scheme}:mcp/install?name=${encodeURIComponent(name)}&config=${encodeURIComponent(JSON.stringify(withType))}`;
50
+ }
51
+ var MCP_CLIENTS = [
52
+ // ── Cursor ──────────────────────────────────────────────────────────────────
53
+ {
54
+ id: "cursor",
55
+ label: "Cursor",
56
+ description: "One-click install into your project's .cursor/mcp.json",
57
+ method: "deeplink",
58
+ transport: "stdio",
59
+ cliIde: "cursor",
60
+ build({ projectId, projectName, apiKey, endpoint }) {
61
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
62
+ const config = buildStdioConfigObj(projectId, apiKey, endpoint);
63
+ return { kind: "deeplink", url: encodeCursorDeeplink(name, config) };
64
+ }
65
+ },
66
+ // ── VS Code ──────────────────────────────────────────────────────────────────
67
+ {
68
+ id: "vscode",
69
+ label: "VS Code",
70
+ description: "One-click install via VS Code MCP extension",
71
+ method: "deeplink",
72
+ transport: "stdio",
73
+ build({ projectId, projectName, apiKey, endpoint }) {
74
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
75
+ const config = buildStdioConfigObj(projectId, apiKey, endpoint);
76
+ return { kind: "deeplink", url: encodeVsCodeDeeplink("vscode", name, config) };
77
+ }
78
+ },
79
+ // ── VS Code Insiders ─────────────────────────────────────────────────────────
80
+ {
81
+ id: "vscode-insiders",
82
+ label: "VS Code Insiders",
83
+ description: "One-click install via VS Code Insiders MCP extension",
84
+ method: "deeplink",
85
+ transport: "stdio",
86
+ build({ projectId, projectName, apiKey, endpoint }) {
87
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
88
+ const config = buildStdioConfigObj(projectId, apiKey, endpoint);
89
+ return { kind: "deeplink", url: encodeVsCodeDeeplink("vscode-insiders", name, config) };
90
+ }
91
+ },
92
+ // ── Windsurf ─────────────────────────────────────────────────────────────────
93
+ {
94
+ id: "windsurf",
95
+ label: "Windsurf",
96
+ description: "Copy config into ~/.codeium/windsurf/mcp_config.json",
97
+ method: "config-json",
98
+ transport: "http",
99
+ build({ projectId, projectName, apiKey, mcpHttpUrl }) {
100
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
101
+ const url = appendFeaturesToUrl(mcpHttpUrl, DEFAULT_FEATURE_GROUPS);
102
+ const headers = {
103
+ Authorization: `Bearer ${apiKey}`,
104
+ "X-Mushi-Api-Key": apiKey
105
+ };
106
+ if (projectId) headers["X-Mushi-Project-Id"] = projectId;
107
+ const windsurfConfig = { type: "http", serverUrl: url, headers };
108
+ const configJson = JSON.stringify(
109
+ { mcpServers: { [name]: windsurfConfig } },
110
+ null,
111
+ 2
112
+ );
113
+ return {
114
+ kind: "config",
115
+ filePath: "~/.codeium/windsurf/mcp_config.json",
116
+ json: configJson
117
+ };
118
+ }
119
+ },
120
+ // ── Cline ────────────────────────────────────────────────────────────────────
121
+ {
122
+ id: "cline",
123
+ label: "Cline",
124
+ description: "Copy config into cline_mcp_settings.json",
125
+ method: "config-json",
126
+ transport: "stdio",
127
+ build({ projectId, projectName, apiKey, endpoint }) {
128
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
129
+ const env = {
130
+ MUSHI_API_ENDPOINT: endpoint,
131
+ MUSHI_API_KEY: apiKey
132
+ };
133
+ if (projectId) env.MUSHI_PROJECT_ID = projectId;
134
+ const clineConfig = {
135
+ mcpServers: {
136
+ [name]: { command: "npx", args: ["-y", "@mushi-mushi/mcp@latest"], env }
137
+ }
138
+ };
139
+ return {
140
+ kind: "config",
141
+ // Cline stores config in the VS Code extension's globalStorage
142
+ filePath: "~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
143
+ json: JSON.stringify(clineConfig, null, 2)
144
+ };
145
+ }
146
+ },
147
+ // ── Claude Code ──────────────────────────────────────────────────────────────
148
+ {
149
+ id: "claude-code",
150
+ label: "Claude Code",
151
+ description: "Add via claude CLI command (HTTP transport)",
152
+ method: "cli-command",
153
+ transport: "http",
154
+ cliIde: "claude",
155
+ build({ projectId, projectName, apiKey, mcpHttpUrl }) {
156
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
157
+ const url = appendFeaturesToUrl(mcpHttpUrl, DEFAULT_FEATURE_GROUPS);
158
+ const cmd = `claude mcp add --transport http ${name} "${url}" --header "Authorization: Bearer ${apiKey}"`;
159
+ return { kind: "command", text: cmd };
160
+ }
161
+ },
162
+ // ── Claude Desktop ───────────────────────────────────────────────────────────
163
+ {
164
+ id: "claude-desktop",
165
+ label: "Claude Desktop",
166
+ description: "Copy config into claude_desktop_config.json",
167
+ method: "config-json",
168
+ transport: "stdio",
169
+ build({ projectId, projectName, apiKey, endpoint }) {
170
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
171
+ const env = {
172
+ MUSHI_API_ENDPOINT: endpoint,
173
+ MUSHI_API_KEY: apiKey
174
+ };
175
+ if (projectId) env.MUSHI_PROJECT_ID = projectId;
176
+ const desktopConfig = {
177
+ mcpServers: {
178
+ [name]: { command: "npx", args: ["-y", "@mushi-mushi/mcp@latest"], env }
179
+ }
180
+ };
181
+ return {
182
+ kind: "config",
183
+ filePath: "~/Library/Application Support/Claude/claude_desktop_config.json",
184
+ json: JSON.stringify(desktopConfig, null, 2)
185
+ };
186
+ }
187
+ },
188
+ // ── Zed ──────────────────────────────────────────────────────────────────────
189
+ {
190
+ id: "zed",
191
+ label: "Zed",
192
+ description: "Copy config into ~/.config/zed/settings.json",
193
+ method: "config-json",
194
+ transport: "stdio",
195
+ cliIde: "zed",
196
+ build({ projectId, projectName, apiKey, endpoint }) {
197
+ const name = projectId ? projectServerName(projectId, projectName) : `mushi-${projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`;
198
+ const env = {
199
+ MUSHI_API_ENDPOINT: endpoint,
200
+ MUSHI_API_KEY: apiKey
201
+ };
202
+ if (projectId) env.MUSHI_PROJECT_ID = projectId;
203
+ const zedConfig = {
204
+ context_servers: {
205
+ [name]: {
206
+ command: {
207
+ path: "npx",
208
+ args: ["-y", "@mushi-mushi/mcp@latest"],
209
+ env
210
+ },
211
+ settings: {}
212
+ }
213
+ }
214
+ };
215
+ return {
216
+ kind: "config",
217
+ filePath: "~/.config/zed/settings.json",
218
+ json: JSON.stringify(zedConfig, null, 2)
219
+ };
220
+ }
221
+ },
222
+ // ── Any MCP client ───────────────────────────────────────────────────────────
223
+ {
224
+ id: "any",
225
+ label: "Any MCP client",
226
+ description: "Streamable HTTP URL \u2014 works with OpenClaw, Hermes, Gemini CLI, ChatGPT, and more",
227
+ method: "remote-url",
228
+ transport: "http",
229
+ build({ projectId, apiKey, mcpHttpUrl }) {
230
+ const url = appendFeaturesToUrl(mcpHttpUrl, DEFAULT_FEATURE_GROUPS);
231
+ const headerSnippet = [
232
+ `Authorization: Bearer ${apiKey}`,
233
+ projectId ? `X-Mushi-Project-Id: ${projectId}` : ""
234
+ ].filter(Boolean).join("\n");
235
+ return { kind: "remote-url", url, headerSnippet };
236
+ }
237
+ }
238
+ ];
239
+ function getMcpClient(id) {
240
+ const client = MCP_CLIENTS.find((c) => c.id === id);
241
+ if (!client) throw new Error(`Unknown MCP client id: ${id}`);
242
+ return client;
243
+ }
244
+ export {
245
+ MCP_CLIENTS,
246
+ buildHttpConfigObj as buildRawHttpConfig,
247
+ buildStdioConfigObj as buildRawStdioConfig,
248
+ getMcpClient,
249
+ projectServerName
250
+ };
package/dist/index.js CHANGED
@@ -933,7 +933,16 @@ function createMushiServer(config) {
933
933
  return body;
934
934
  }
935
935
  async function resolveProjectId(explicitId) {
936
- if (explicitId) return explicitId;
936
+ if (explicitId) {
937
+ if (projectId && explicitId !== projectId) {
938
+ throw new MushiApiError(
939
+ 400,
940
+ "PROJECT_SCOPE_MISMATCH",
941
+ `This MCP entry is bound to project ${projectId}. Either pass projectId="${projectId}", add a second MCP server entry for ${explicitId} (Console \u2192 Connect \u2192 Add to Cursor while that project is selected), or run \`mushi setup --all-projects --ide cursor\` for one entry per repo.`
942
+ );
943
+ }
944
+ return explicitId;
945
+ }
937
946
  if (projectId) return projectId;
938
947
  try {
939
948
  const data = await apiCall(
@@ -964,6 +973,11 @@ function createMushiServer(config) {
964
973
  );
965
974
  }
966
975
  }
976
+ async function projectScopeHeaders(explicitId) {
977
+ const pid = await resolveProjectId(explicitId ?? void 0);
978
+ const headers = { "X-Mushi-Project-Id": pid };
979
+ return { projectId: pid, headers };
980
+ }
967
981
  function jsonText(value) {
968
982
  return {
969
983
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
@@ -1045,11 +1059,15 @@ function createMushiServer(config) {
1045
1059
  title: titleOf("get_report_detail"),
1046
1060
  description: descOf("get_report_detail"),
1047
1061
  annotations: annotationsFor("get_report_detail"),
1048
- inputSchema: { reportId: z.string().describe("The report UUID") },
1062
+ inputSchema: {
1063
+ reportId: z.string().describe("The report UUID"),
1064
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1065
+ },
1049
1066
  outputSchema: { report: z.unknown() }
1050
1067
  },
1051
1068
  async (args) => {
1052
- const data = await apiCall(`/v1/admin/reports/${args.reportId}`);
1069
+ const { headers } = await projectScopeHeaders(args.project_id);
1070
+ const data = await apiCall(`/v1/admin/reports/${args.reportId}`, { headers });
1053
1071
  const res = jsonResult({ report: data });
1054
1072
  return {
1055
1073
  ...res,
@@ -1140,7 +1158,10 @@ function createMushiServer(config) {
1140
1158
  title: titleOf("get_fix_context"),
1141
1159
  description: descOf("get_fix_context"),
1142
1160
  annotations: annotationsFor("get_fix_context"),
1143
- inputSchema: { reportId: z.string().describe("The report UUID to fix") },
1161
+ inputSchema: {
1162
+ reportId: z.string().describe("The report UUID to fix"),
1163
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1164
+ },
1144
1165
  outputSchema: {
1145
1166
  report: z.unknown(),
1146
1167
  fixPrompt: z.unknown(),
@@ -1151,7 +1172,10 @@ function createMushiServer(config) {
1151
1172
  }
1152
1173
  },
1153
1174
  async (args) => {
1154
- const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
1175
+ const { headers } = await projectScopeHeaders(args.project_id);
1176
+ const report = await apiCall(`/v1/admin/reports/${args.reportId}`, {
1177
+ headers
1178
+ });
1155
1179
  return jsonResult({
1156
1180
  report,
1157
1181
  // Paste-ready fix prompt composed server-side by composeFixPacket()
@@ -1171,9 +1195,15 @@ function createMushiServer(config) {
1171
1195
  title: titleOf("get_fix_timeline"),
1172
1196
  description: descOf("get_fix_timeline"),
1173
1197
  annotations: annotationsFor("get_fix_timeline"),
1174
- inputSchema: { fixId: z.string().describe("fix_attempt UUID") }
1198
+ inputSchema: {
1199
+ fixId: z.string().describe("fix_attempt UUID"),
1200
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1201
+ }
1175
1202
  },
1176
- async (args) => jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`))
1203
+ async (args) => {
1204
+ const { headers } = await projectScopeHeaders(args.project_id);
1205
+ return jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`, { headers }));
1206
+ }
1177
1207
  );
1178
1208
  server.registerTool(
1179
1209
  "get_blast_radius",
@@ -1803,18 +1833,19 @@ function createMushiServer(config) {
1803
1833
  }
1804
1834
  },
1805
1835
  async (args) => {
1806
- const pid = args.project_id ?? projectId;
1836
+ const { projectId: pid, headers } = await projectScopeHeaders(args.project_id);
1807
1837
  const includeLogs = args.include_logs !== false;
1808
1838
  const [reportRes, evidenceRes, similarRes, fixCtxRes, blastRes, logsRes] = await Promise.allSettled([
1809
- apiCall(`/v1/admin/reports/${args.report_id}`),
1810
- apiCall(`/v1/admin/reports/${args.report_id}/timeline`),
1839
+ apiCall(`/v1/admin/reports/${args.report_id}`, { headers }),
1840
+ apiCall(`/v1/admin/reports/${args.report_id}/timeline`, { headers }),
1811
1841
  apiCall(`/v1/admin/reports/similarity`, {
1812
1842
  method: "POST",
1843
+ headers,
1813
1844
  body: JSON.stringify({ report_id: args.report_id })
1814
1845
  }).catch(() => null),
1815
- pid ? apiCall(`/v1/admin/reports/${args.report_id}/fix-context`).catch(() => null) : Promise.resolve(null),
1816
- pid ? apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`).catch(() => null) : Promise.resolve(null),
1817
- includeLogs && pid ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`).catch(() => null) : Promise.resolve(null)
1846
+ apiCall(`/v1/admin/reports/${args.report_id}/fix-context`, { headers }).catch(() => null),
1847
+ apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`, { headers }).catch(() => null),
1848
+ includeLogs ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`, { headers }).catch(() => null) : Promise.resolve(null)
1818
1849
  ]);
1819
1850
  const report = reportRes.status === "fulfilled" ? reportRes.value : null;
1820
1851
  const severity = report ? report.severity : "unknown";
package/dist/server.js CHANGED
@@ -199,7 +199,16 @@ function createMushiServer(config) {
199
199
  return body;
200
200
  }
201
201
  async function resolveProjectId(explicitId) {
202
- if (explicitId) return explicitId;
202
+ if (explicitId) {
203
+ if (projectId && explicitId !== projectId) {
204
+ throw new MushiApiError(
205
+ 400,
206
+ "PROJECT_SCOPE_MISMATCH",
207
+ `This MCP entry is bound to project ${projectId}. Either pass projectId="${projectId}", add a second MCP server entry for ${explicitId} (Console \u2192 Connect \u2192 Add to Cursor while that project is selected), or run \`mushi setup --all-projects --ide cursor\` for one entry per repo.`
208
+ );
209
+ }
210
+ return explicitId;
211
+ }
203
212
  if (projectId) return projectId;
204
213
  try {
205
214
  const data = await apiCall(
@@ -230,6 +239,11 @@ function createMushiServer(config) {
230
239
  );
231
240
  }
232
241
  }
242
+ async function projectScopeHeaders(explicitId) {
243
+ const pid = await resolveProjectId(explicitId ?? void 0);
244
+ const headers = { "X-Mushi-Project-Id": pid };
245
+ return { projectId: pid, headers };
246
+ }
233
247
  function jsonText(value) {
234
248
  return {
235
249
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
@@ -311,11 +325,15 @@ function createMushiServer(config) {
311
325
  title: titleOf("get_report_detail"),
312
326
  description: descOf("get_report_detail"),
313
327
  annotations: annotationsFor("get_report_detail"),
314
- inputSchema: { reportId: z.string().describe("The report UUID") },
328
+ inputSchema: {
329
+ reportId: z.string().describe("The report UUID"),
330
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
331
+ },
315
332
  outputSchema: { report: z.unknown() }
316
333
  },
317
334
  async (args) => {
318
- const data = await apiCall(`/v1/admin/reports/${args.reportId}`);
335
+ const { headers } = await projectScopeHeaders(args.project_id);
336
+ const data = await apiCall(`/v1/admin/reports/${args.reportId}`, { headers });
319
337
  const res = jsonResult({ report: data });
320
338
  return {
321
339
  ...res,
@@ -406,7 +424,10 @@ function createMushiServer(config) {
406
424
  title: titleOf("get_fix_context"),
407
425
  description: descOf("get_fix_context"),
408
426
  annotations: annotationsFor("get_fix_context"),
409
- inputSchema: { reportId: z.string().describe("The report UUID to fix") },
427
+ inputSchema: {
428
+ reportId: z.string().describe("The report UUID to fix"),
429
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
430
+ },
410
431
  outputSchema: {
411
432
  report: z.unknown(),
412
433
  fixPrompt: z.unknown(),
@@ -417,7 +438,10 @@ function createMushiServer(config) {
417
438
  }
418
439
  },
419
440
  async (args) => {
420
- const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
441
+ const { headers } = await projectScopeHeaders(args.project_id);
442
+ const report = await apiCall(`/v1/admin/reports/${args.reportId}`, {
443
+ headers
444
+ });
421
445
  return jsonResult({
422
446
  report,
423
447
  // Paste-ready fix prompt composed server-side by composeFixPacket()
@@ -437,9 +461,15 @@ function createMushiServer(config) {
437
461
  title: titleOf("get_fix_timeline"),
438
462
  description: descOf("get_fix_timeline"),
439
463
  annotations: annotationsFor("get_fix_timeline"),
440
- inputSchema: { fixId: z.string().describe("fix_attempt UUID") }
464
+ inputSchema: {
465
+ fixId: z.string().describe("fix_attempt UUID"),
466
+ project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
467
+ }
441
468
  },
442
- async (args) => jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`))
469
+ async (args) => {
470
+ const { headers } = await projectScopeHeaders(args.project_id);
471
+ return jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`, { headers }));
472
+ }
443
473
  );
444
474
  server.registerTool(
445
475
  "get_blast_radius",
@@ -1069,18 +1099,19 @@ function createMushiServer(config) {
1069
1099
  }
1070
1100
  },
1071
1101
  async (args) => {
1072
- const pid = args.project_id ?? projectId;
1102
+ const { projectId: pid, headers } = await projectScopeHeaders(args.project_id);
1073
1103
  const includeLogs = args.include_logs !== false;
1074
1104
  const [reportRes, evidenceRes, similarRes, fixCtxRes, blastRes, logsRes] = await Promise.allSettled([
1075
- apiCall(`/v1/admin/reports/${args.report_id}`),
1076
- apiCall(`/v1/admin/reports/${args.report_id}/timeline`),
1105
+ apiCall(`/v1/admin/reports/${args.report_id}`, { headers }),
1106
+ apiCall(`/v1/admin/reports/${args.report_id}/timeline`, { headers }),
1077
1107
  apiCall(`/v1/admin/reports/similarity`, {
1078
1108
  method: "POST",
1109
+ headers,
1079
1110
  body: JSON.stringify({ report_id: args.report_id })
1080
1111
  }).catch(() => null),
1081
- pid ? apiCall(`/v1/admin/reports/${args.report_id}/fix-context`).catch(() => null) : Promise.resolve(null),
1082
- pid ? apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`).catch(() => null) : Promise.resolve(null),
1083
- includeLogs && pid ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`).catch(() => null) : Promise.resolve(null)
1112
+ apiCall(`/v1/admin/reports/${args.report_id}/fix-context`, { headers }).catch(() => null),
1113
+ apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`, { headers }).catch(() => null),
1114
+ includeLogs ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`, { headers }).catch(() => null) : Promise.resolve(null)
1084
1115
  ]);
1085
1116
  const report = reportRes.status === "fulfilled" ? reportRes.value : null;
1086
1117
  const severity = report ? report.severity : "unknown";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mushi-mushi/mcp",
3
3
  "mcpName": "io.github.kensaurus/mushi-mushi",
4
- "version": "0.16.2",
4
+ "version": "0.17.0",
5
5
  "license": "MIT",
6
6
  "description": "MCP server for Mushi Mushi — triage user-felt bugs, pull evidence (screenshots, console logs, network requests), check blast radius, and dispatch fix PRs, all from your AI coding agent",
7
7
  "type": "module",
@@ -25,6 +25,10 @@
25
25
  "./feature-groups": {
26
26
  "types": "./dist/feature-groups.d.ts",
27
27
  "import": "./dist/feature-groups.js"
28
+ },
29
+ "./clients": {
30
+ "types": "./dist/clients.d.ts",
31
+ "import": "./dist/clients.js"
28
32
  }
29
33
  },
30
34
  "main": "./dist/index.js",
@@ -43,7 +47,7 @@
43
47
  "dependencies": {
44
48
  "@modelcontextprotocol/sdk": "^1.29.0",
45
49
  "zod": "^4.4.3",
46
- "@mushi-mushi/core": "^1.20.0"
50
+ "@mushi-mushi/core": "^1.21.0"
47
51
  },
48
52
  "devDependencies": {
49
53
  "@types/node": "^22.19.21",