@mushi-mushi/mcp 0.23.0 → 0.24.1
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 +4 -3
- package/dist/catalog.js +1 -1
- package/dist/{chunk-CXSLU5V5.js → chunk-F65T4IGT.js} +44 -0
- package/dist/{chunk-EFENR3EA.js → chunk-IC7PPRRZ.js} +331 -6
- package/dist/clients.d.ts +1 -1
- package/dist/clients.js +2 -2
- package/dist/feature-groups.js +1 -1
- package/dist/index.js +1260 -78
- package/dist/server.js +806 -20
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
TDD_TOOL_CATALOG,
|
|
5
5
|
TOOL_CATALOG,
|
|
6
6
|
routeUseMushiIntent
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IC7PPRRZ.js";
|
|
8
8
|
import {
|
|
9
9
|
MUSHI_SERVER_METADATA
|
|
10
10
|
} from "./chunk-FJRPC4VR.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
DEPRECATED_TOOL_ALIASES,
|
|
13
13
|
TOOL_FEATURE_MAP,
|
|
14
14
|
toolMatchesFeatures
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-F65T4IGT.js";
|
|
16
16
|
|
|
17
17
|
// src/server.ts
|
|
18
18
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -185,7 +185,7 @@ var MUSHI_DOCS_INDEX = [
|
|
|
185
185
|
route: "/admin/explore",
|
|
186
186
|
markdown: "admin/explore.md",
|
|
187
187
|
keywords: ["admin", "explore", "codebase", "atlas"],
|
|
188
|
-
headings: ["Tab groups", "Map views", "Graph", "Layers", "Search", "The detail panel"
|
|
188
|
+
headings: ["Tab groups", "Copy digest", "Map views", "Diagram", "Graph", "Layers", "Search", "The detail panel"],
|
|
189
189
|
excerpt: "Visual, searchable map of every indexed source file \u2014 explore architectural layers, import dependencies, and find code by meaning."
|
|
190
190
|
},
|
|
191
191
|
{
|
|
@@ -221,7 +221,7 @@ var MUSHI_DOCS_INDEX = [
|
|
|
221
221
|
route: "/admin/fixes",
|
|
222
222
|
markdown: "admin/fixes.md",
|
|
223
223
|
keywords: ["admin", "fixes", "fix", "drafts", "prs"],
|
|
224
|
-
headings: ["The fixes list", "Streaming view", "Spec-validation warnings", "Cursor Cloud Agent runs", "Dispatching a fix", "Webhooks", "CI feedback on open PRs"
|
|
224
|
+
headings: ["The fixes list", "Streaming view", "Spec-validation warnings", "Cursor Cloud Agent runs", "Notes-only pull requests", "Dispatching a fix", "Webhooks", "CI feedback on open PRs"],
|
|
225
225
|
excerpt: "The Fixes page tracks every draft pull request Mushi opened from a bug report, from the moment a fix run starts until the PR is ready or the run fails."
|
|
226
226
|
},
|
|
227
227
|
{
|
|
@@ -230,7 +230,7 @@ var MUSHI_DOCS_INDEX = [
|
|
|
230
230
|
route: "/admin/fullstack-audit",
|
|
231
231
|
markdown: "admin/fullstack-audit.md",
|
|
232
232
|
keywords: ["admin", "fullstack-audit", "fullstack", "audit", "full-stack"],
|
|
233
|
-
headings: ["Scorecard sections", "Prerequisites", "CLI", "Related pages"],
|
|
233
|
+
headings: ["Scorecard sections", "Open findings by check", 'Mushi setup checks and "Apply suggested caps"', "Prerequisites", "CLI", "Related pages"],
|
|
234
234
|
excerpt: "The Full-stack audit runs one health check across RLS gaps, recent backend errors and API contract drift, and returns a readable scorecard."
|
|
235
235
|
},
|
|
236
236
|
{
|
|
@@ -368,6 +368,15 @@ var MUSHI_DOCS_INDEX = [
|
|
|
368
368
|
headings: ["What you see", "Data source", "Related"],
|
|
369
369
|
excerpt: "The Overview page is the organization-wide portfolio view \u2014 seven-day activity and open tickets for every connected project at a glance."
|
|
370
370
|
},
|
|
371
|
+
{
|
|
372
|
+
title: "Portfolio",
|
|
373
|
+
url: docUrl("/admin/portfolio"),
|
|
374
|
+
route: "/admin/portfolio",
|
|
375
|
+
markdown: "admin/portfolio.md",
|
|
376
|
+
keywords: ["admin", "portfolio"],
|
|
377
|
+
headings: ["What you see", "When something could not be read", "Spend per app", "Daily digest", "Accounts and resilience", "From your editor", "Who can see it"],
|
|
378
|
+
excerpt: "The Portfolio page shows every app in a team at once, with each app's worst recipe state, hole checks and SDK version, plus fixes that apply across apps."
|
|
379
|
+
},
|
|
371
380
|
{
|
|
372
381
|
title: "Projects",
|
|
373
382
|
url: docUrl("/admin/projects"),
|
|
@@ -428,7 +437,7 @@ var MUSHI_DOCS_INDEX = [
|
|
|
428
437
|
route: "/admin/releases",
|
|
429
438
|
markdown: "admin/releases.md",
|
|
430
439
|
keywords: ["admin", "releases"],
|
|
431
|
-
headings: ["What it generates", "Creating a release", "Release entry fields", "SDK surface", "Related pages"],
|
|
440
|
+
headings: ["What it generates", "Creating a release", "Release automatically when you ship", "Release entry fields", "SDK surface", "Related pages"],
|
|
432
441
|
excerpt: "The Releases page drafts a changelog for each version from the fixes that shipped, crediting the users whose reports led to each fix."
|
|
433
442
|
},
|
|
434
443
|
{
|
|
@@ -683,6 +692,15 @@ var MUSHI_DOCS_INDEX = [
|
|
|
683
692
|
headings: ["Reporter reputation", "Anti-gaming detection", "Report fatigue prevention"],
|
|
684
693
|
excerpt: "How Mushi keeps bug reports honest \u2014 reporter reputation against spam and point farming, and prompt-fatigue limits so real users are not nagged."
|
|
685
694
|
},
|
|
695
|
+
{
|
|
696
|
+
title: "App recipe and design system",
|
|
697
|
+
url: docUrl("/concepts/app-recipe"),
|
|
698
|
+
route: "/concepts/app-recipe",
|
|
699
|
+
markdown: "concepts/app-recipe.md",
|
|
700
|
+
keywords: ["concepts", "app-recipe", "app", "recipe", "design", "system"],
|
|
701
|
+
headings: ["The five states", "mushi.recipe.json", "The design system page", "Directions", "The deviance check", "The deviance score", "When the score is too high", "Hole checks"],
|
|
702
|
+
excerpt: "The App Recipe records what your app is made of, so a diagnosis can say what changed and a fix can respect your design system."
|
|
703
|
+
},
|
|
686
704
|
{
|
|
687
705
|
title: "Architecture",
|
|
688
706
|
url: docUrl("/concepts/architecture"),
|
|
@@ -827,6 +845,15 @@ var MUSHI_DOCS_INDEX = [
|
|
|
827
845
|
headings: ["Setup default", "When host init wins", "Console tuning without rebuild", "Widget draft persistence", "Troubleshooting", "Related"],
|
|
828
846
|
excerpt: "How the Mushi SDK fetches widget and capture settings from the console at startup, how they merge with your init options, and when to turn it off."
|
|
829
847
|
},
|
|
848
|
+
{
|
|
849
|
+
title: "One login and one purchase across your apps",
|
|
850
|
+
url: docUrl("/concepts/shared-login"),
|
|
851
|
+
route: "/concepts/shared-login",
|
|
852
|
+
markdown: "concepts/shared-login.md",
|
|
853
|
+
keywords: ["concepts", "shared-login", "shared", "login", "one", "purchase", "across", "apps"],
|
|
854
|
+
headings: ["Signing in once", "What Mushi checks", "Keeping a purchase in every app", "Sending users between your apps", "Comparing apps"],
|
|
855
|
+
excerpt: "How to let people sign in once and keep what they bought across several of your apps, and what Mushi checks so it keeps working."
|
|
856
|
+
},
|
|
830
857
|
{
|
|
831
858
|
title: "Where the report button lives",
|
|
832
859
|
url: docUrl("/concepts/trigger-modes"),
|
|
@@ -1211,7 +1238,7 @@ var MUSHI_DOCS_INDEX = [
|
|
|
1211
1238
|
route: "/plugins/slack",
|
|
1212
1239
|
markdown: "plugins/slack.md",
|
|
1213
1240
|
keywords: ["plugins", "slack", "app"],
|
|
1214
|
-
headings: ["Setup", "What's on the card", "Acting from the channel", "Package", "Events", "Troubleshooting"],
|
|
1241
|
+
headings: ["Setup", "What's on the card", "Acting from the channel", "Slash commands", "Package", "Events", "Troubleshooting"],
|
|
1215
1242
|
excerpt: "Triage Mushi bug reports from Slack \u2014 each classified report posts as a card with its root cause and buttons to dispatch a fix, resolve or dismiss."
|
|
1216
1243
|
},
|
|
1217
1244
|
{
|
|
@@ -1337,7 +1364,7 @@ var MUSHI_DOCS_INDEX = [
|
|
|
1337
1364
|
route: "/quickstart/react-native",
|
|
1338
1365
|
markdown: "quickstart/react-native.md",
|
|
1339
1366
|
keywords: ["quickstart", "react-native", "react", "native"],
|
|
1340
|
-
headings: ["1. Install", "2. Mount the provider", "3. Verify"],
|
|
1367
|
+
headings: ["1. Install", "2. Mount the provider", "Optional native modules", "3. Verify"],
|
|
1341
1368
|
excerpt: "Add shake-to-report bug capture to a React Native or Expo app with @mushi-mushi/react-native \u2014 install, mount the provider, and verify the first report."
|
|
1342
1369
|
},
|
|
1343
1370
|
{
|
|
@@ -1454,7 +1481,7 @@ var MUSHI_DOCS_INDEX = [
|
|
|
1454
1481
|
route: "/sdks/cli",
|
|
1455
1482
|
markdown: "sdks/cli.md",
|
|
1456
1483
|
keywords: ["sdks", "cli", "@mushi-mushi/cli"],
|
|
1457
|
-
headings: ["Console \u2194 CLI setup loop", "Setup & health", "mushi doctor", "mushi nudge", "Reports & lessons", "
|
|
1484
|
+
headings: ["Console \u2194 CLI setup loop", "Setup & health", "mushi doctor", "mushi nudge", "Reports & lessons", "Sentry issues already open", "Reporter updates held for review", "Agentic fixes"],
|
|
1458
1485
|
excerpt: "Reference for @mushi-mushi/cli \u2014 log in, connect a project, run doctor, list reports, dispatch agent fixes and merge their pull requests from the terminal."
|
|
1459
1486
|
},
|
|
1460
1487
|
{
|
|
@@ -2140,6 +2167,43 @@ function fixContextOf(report) {
|
|
|
2140
2167
|
bugOntologyTags: report.bug_ontology_tags ?? null
|
|
2141
2168
|
};
|
|
2142
2169
|
}
|
|
2170
|
+
var RECIPE_STATES = ["ok", "drift", "unknown", "not_connected", "error"];
|
|
2171
|
+
var DESIGN_EXCERPT_MAX_FILES = 20;
|
|
2172
|
+
function designExcerptFilesOf(report) {
|
|
2173
|
+
const out = [];
|
|
2174
|
+
const add = (path) => {
|
|
2175
|
+
if (typeof path !== "string") return;
|
|
2176
|
+
const p = path.trim();
|
|
2177
|
+
if (!p || p.includes(",") || out.includes(p) || out.length >= DESIGN_EXCERPT_MAX_FILES) return;
|
|
2178
|
+
out.push(p);
|
|
2179
|
+
};
|
|
2180
|
+
const attempts = Array.isArray(report.fix_attempts) ? report.fix_attempts : [];
|
|
2181
|
+
for (const attempt of attempts) {
|
|
2182
|
+
if (Array.isArray(attempt?.files_changed)) attempt.files_changed.forEach(add);
|
|
2183
|
+
}
|
|
2184
|
+
if (typeof report.fix_packet === "string") {
|
|
2185
|
+
for (const m of report.fix_packet.matchAll(/^### `([^`\n]+)`$/gm)) add(m[1]);
|
|
2186
|
+
}
|
|
2187
|
+
return out;
|
|
2188
|
+
}
|
|
2189
|
+
function designExcerptQueryOf(report) {
|
|
2190
|
+
const params = new URLSearchParams();
|
|
2191
|
+
const files = designExcerptFilesOf(report);
|
|
2192
|
+
if (files.length > 0) params.set("files", files.join(","));
|
|
2193
|
+
if (typeof report.id === "string" && /^[0-9a-f-]{36}$/i.test(report.id)) params.set("reportId", report.id);
|
|
2194
|
+
const query = params.toString();
|
|
2195
|
+
return query ? `?${query}` : "";
|
|
2196
|
+
}
|
|
2197
|
+
function recipeFromExcerpt(excerpt) {
|
|
2198
|
+
const row = excerpt && typeof excerpt === "object" ? excerpt : null;
|
|
2199
|
+
if (!row || !RECIPE_STATES.includes(row.state)) {
|
|
2200
|
+
return recipeError("The design excerpt came back without a state.");
|
|
2201
|
+
}
|
|
2202
|
+
return typeof row.note === "string" ? row : { ...row, note: "" };
|
|
2203
|
+
}
|
|
2204
|
+
function recipeError(note) {
|
|
2205
|
+
return { state: "error", note };
|
|
2206
|
+
}
|
|
2143
2207
|
function similarityQueryOf(report) {
|
|
2144
2208
|
for (const field of ["summary", "description"]) {
|
|
2145
2209
|
const value = report[field];
|
|
@@ -2336,7 +2400,15 @@ var GATE_IDS = [
|
|
|
2336
2400
|
"orphan_endpoint",
|
|
2337
2401
|
"unknown_call",
|
|
2338
2402
|
"schema_drift",
|
|
2339
|
-
"code_health"
|
|
2403
|
+
"code_health",
|
|
2404
|
+
"design_drift",
|
|
2405
|
+
"ci_drift",
|
|
2406
|
+
"deploy_drift",
|
|
2407
|
+
"env_drift",
|
|
2408
|
+
"radar",
|
|
2409
|
+
"portfolio_radar",
|
|
2410
|
+
"portfolio_radar_ci",
|
|
2411
|
+
"store_review"
|
|
2340
2412
|
];
|
|
2341
2413
|
var GATE_FINDING_SEVERITIES = ["info", "warn", "error"];
|
|
2342
2414
|
var SKILL_CATEGORIES = [
|
|
@@ -2401,6 +2473,22 @@ var FIX_CONTEXT_SHAPE = {
|
|
|
2401
2473
|
rootCause: z.unknown().describe("Stage-2 root cause, or null before Stage 2 runs"),
|
|
2402
2474
|
bugOntologyTags: z.unknown().describe("Bug ontology tags, or null")
|
|
2403
2475
|
};
|
|
2476
|
+
var RECIPE_EXCERPT_OUTPUT = z.looseObject({
|
|
2477
|
+
state: z.enum(RECIPE_STATES).describe("ok | drift | unknown | not_connected | error; unknown never means healthy"),
|
|
2478
|
+
note: z.string().describe("Why this state, in plain English"),
|
|
2479
|
+
set: z.string().nullable().optional().describe("The token set the excerpt was taken from"),
|
|
2480
|
+
tokens: z.array(z.unknown()).optional().describe("Most useful tokens first: { path, value, cssVar, ts }. Use these names, not literals."),
|
|
2481
|
+
findings: z.array(z.unknown()).optional().describe("Open design deviance findings in the files this fix touches"),
|
|
2482
|
+
score: z.number().nullable().optional().describe("Design deviance score 0\u2013100, lower is better; null = not scored"),
|
|
2483
|
+
truncated: z.boolean().optional().describe("True when the excerpt was cut to stay under 4 KB"),
|
|
2484
|
+
context: z.looseObject({
|
|
2485
|
+
schema: z.unknown().describe("{ state, note, snapshotAt, tables: [{ name, columns }], missing }: tables the stack trace names, from the latest schema snapshot"),
|
|
2486
|
+
deploy: z.unknown().describe("{ state: live | deployed_since_merge | not_live | probe_failed | unknown | no_merged_fix | error, note, lastFix, targets }"),
|
|
2487
|
+
radar: z.unknown().describe("{ state, note, checkedAt, findings: [{ rule, severity, message, fix }] }: open hole checks")
|
|
2488
|
+
}).optional().describe("Backend and release context for this bug; each section has its own state, and unknown never means healthy")
|
|
2489
|
+
}).describe(
|
|
2490
|
+
"Design excerpt (\u2264 4 KB) for the report's project: tokens with CSS var / TS names, the deviance score, findings in the files this fix touches, and context (tables the stack trace names, whether the last fix is live, open hole checks). Never null; when it could not be read, { state: 'error', note }."
|
|
2491
|
+
);
|
|
2404
2492
|
function isWrappedUntrusted(text) {
|
|
2405
2493
|
return text.startsWith('<mushi-data role="');
|
|
2406
2494
|
}
|
|
@@ -2423,10 +2511,11 @@ function createMushiServer(config) {
|
|
|
2423
2511
|
icons: [...MUSHI_SERVER_METADATA.icons]
|
|
2424
2512
|
});
|
|
2425
2513
|
}
|
|
2426
|
-
async function apiCall(path, options) {
|
|
2514
|
+
async function apiCall(path, options, callOptions) {
|
|
2427
2515
|
const requestId = crypto.randomUUID().slice(0, 12);
|
|
2428
2516
|
const started = Date.now();
|
|
2429
|
-
const
|
|
2517
|
+
const effectiveTimeoutMs = Math.max(timeoutMs, callOptions?.minTimeoutMs ?? 0);
|
|
2518
|
+
const timeoutSignal = AbortSignal.timeout(effectiveTimeoutMs);
|
|
2430
2519
|
const toolCall = toolCallContext.getStore();
|
|
2431
2520
|
let res;
|
|
2432
2521
|
try {
|
|
@@ -2463,11 +2552,11 @@ function createMushiServer(config) {
|
|
|
2463
2552
|
function classifyTransportError(err) {
|
|
2464
2553
|
const durationMs2 = Date.now() - started;
|
|
2465
2554
|
if (timeoutSignal.aborted) {
|
|
2466
|
-
apiLog.warn("api.timeout", { path, requestId, durationMs: durationMs2, timeoutMs });
|
|
2555
|
+
apiLog.warn("api.timeout", { path, requestId, durationMs: durationMs2, timeoutMs: effectiveTimeoutMs });
|
|
2467
2556
|
return new MushiApiError(
|
|
2468
2557
|
504,
|
|
2469
2558
|
"MUSHI_TIMEOUT",
|
|
2470
|
-
`Timed out after ${
|
|
2559
|
+
`Timed out after ${effectiveTimeoutMs}ms waiting for ${path}. The Mushi API accepted the connection but did not answer in time \u2014 retry, or raise the budget with MUSHI_MCP_TIMEOUT_MS (ms) in your MCP env block if you self-host and cold starts are slow. Endpoint: ${apiEndpoint}`
|
|
2471
2560
|
);
|
|
2472
2561
|
}
|
|
2473
2562
|
const name = err instanceof Error ? err.name : "";
|
|
@@ -2606,6 +2695,19 @@ function createMushiServer(config) {
|
|
|
2606
2695
|
structuredContent: value
|
|
2607
2696
|
};
|
|
2608
2697
|
}
|
|
2698
|
+
async function designRecipeFor(report, fallbackProjectId, headers) {
|
|
2699
|
+
const pid = typeof report.project_id === "string" && report.project_id ? report.project_id : fallbackProjectId;
|
|
2700
|
+
const query = designExcerptQueryOf(report);
|
|
2701
|
+
try {
|
|
2702
|
+
return recipeFromExcerpt(
|
|
2703
|
+
await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/excerpt${query}`, {
|
|
2704
|
+
headers
|
|
2705
|
+
})
|
|
2706
|
+
);
|
|
2707
|
+
} catch (err) {
|
|
2708
|
+
return recipeError(`Could not read the design excerpt: ${bareMessage(err)}`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2609
2711
|
const server = new McpServer(
|
|
2610
2712
|
{
|
|
2611
2713
|
name: MUSHI_SERVER_METADATA.name,
|
|
@@ -2811,19 +2913,22 @@ function createMushiServer(config) {
|
|
|
2811
2913
|
outputSchema: z.object({
|
|
2812
2914
|
report: REPORT_DETAIL_OUTPUT,
|
|
2813
2915
|
...FIX_CONTEXT_SHAPE,
|
|
2814
|
-
inventoryAction: z.unknown().describe("The inventory action (with its expected_outcome contract) the report is filed against, or null")
|
|
2916
|
+
inventoryAction: z.unknown().describe("The inventory action (with its expected_outcome contract) the report is filed against, or null"),
|
|
2917
|
+
recipe: RECIPE_EXCERPT_OUTPUT
|
|
2815
2918
|
})
|
|
2816
2919
|
},
|
|
2817
2920
|
async (args) => {
|
|
2818
|
-
const { headers } = await projectScopeHeaders(args.projectId);
|
|
2921
|
+
const { projectId: scopedProjectId, headers } = await projectScopeHeaders(args.projectId);
|
|
2819
2922
|
const report = await apiCall(`/v1/admin/reports/${args.reportId}`, {
|
|
2820
2923
|
headers
|
|
2821
2924
|
});
|
|
2925
|
+
const recipe = await designRecipeFor(report, scopedProjectId, headers);
|
|
2822
2926
|
return wrappedJsonResult(
|
|
2823
2927
|
{
|
|
2824
2928
|
report: projectReportDetail(report, false),
|
|
2825
2929
|
...fixContextOf(report),
|
|
2826
|
-
inventoryAction: report.inventory_action ?? null
|
|
2930
|
+
inventoryAction: report.inventory_action ?? null,
|
|
2931
|
+
recipe
|
|
2827
2932
|
},
|
|
2828
2933
|
"fix context"
|
|
2829
2934
|
);
|
|
@@ -2998,6 +3103,598 @@ function createMushiServer(config) {
|
|
|
2998
3103
|
return jsonResult(data);
|
|
2999
3104
|
}
|
|
3000
3105
|
);
|
|
3106
|
+
server.registerTool(
|
|
3107
|
+
"get_app_recipe",
|
|
3108
|
+
{
|
|
3109
|
+
title: titleOf("get_app_recipe"),
|
|
3110
|
+
description: descOf("get_app_recipe"),
|
|
3111
|
+
annotations: annotationsFor("get_app_recipe"),
|
|
3112
|
+
inputSchema: z.object({
|
|
3113
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
|
|
3114
|
+
})
|
|
3115
|
+
},
|
|
3116
|
+
async (args) => {
|
|
3117
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3118
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe`));
|
|
3119
|
+
}
|
|
3120
|
+
);
|
|
3121
|
+
server.registerTool(
|
|
3122
|
+
"get_design_tokens",
|
|
3123
|
+
{
|
|
3124
|
+
title: titleOf("get_design_tokens"),
|
|
3125
|
+
description: descOf("get_design_tokens"),
|
|
3126
|
+
annotations: annotationsFor("get_design_tokens"),
|
|
3127
|
+
inputSchema: z.object({
|
|
3128
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
|
|
3129
|
+
group: z.string().max(80).optional().describe("Only tokens in this group (first path segment, e.g. color, space, radius)."),
|
|
3130
|
+
type: z.string().max(40).optional().describe("Only tokens of this DTCG $type (color, dimension, fontFamily, \u2026)."),
|
|
3131
|
+
direction: z.string().max(80).optional().describe("A named token set (directions/<name>/) instead of the active one.")
|
|
3132
|
+
})
|
|
3133
|
+
},
|
|
3134
|
+
async (args) => {
|
|
3135
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3136
|
+
const q = new URLSearchParams();
|
|
3137
|
+
if (args.group) q.set("group", args.group);
|
|
3138
|
+
if (args.type) q.set("type", args.type);
|
|
3139
|
+
if (args.direction) q.set("direction", args.direction);
|
|
3140
|
+
const suffix = q.toString() ? `?${q}` : "";
|
|
3141
|
+
return jsonText(
|
|
3142
|
+
await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/tokens${suffix}`)
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
);
|
|
3146
|
+
server.registerTool(
|
|
3147
|
+
"get_design_deviance",
|
|
3148
|
+
{
|
|
3149
|
+
title: titleOf("get_design_deviance"),
|
|
3150
|
+
description: descOf("get_design_deviance"),
|
|
3151
|
+
annotations: annotationsFor("get_design_deviance"),
|
|
3152
|
+
inputSchema: z.object({
|
|
3153
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
|
|
3154
|
+
limit: z.number().int().min(1).max(200).optional().describe("Findings to return, 1\u2013200 (default 25).")
|
|
3155
|
+
})
|
|
3156
|
+
},
|
|
3157
|
+
async (args) => {
|
|
3158
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3159
|
+
const suffix = args.limit !== void 0 ? `?${new URLSearchParams({ limit: String(args.limit) })}` : "";
|
|
3160
|
+
return jsonText(
|
|
3161
|
+
await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/deviance${suffix}`)
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
);
|
|
3165
|
+
const ORG_ID_INPUT = z.object({
|
|
3166
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization when omitted")
|
|
3167
|
+
});
|
|
3168
|
+
server.registerTool(
|
|
3169
|
+
"get_portfolio",
|
|
3170
|
+
{
|
|
3171
|
+
title: titleOf("get_portfolio"),
|
|
3172
|
+
description: descOf("get_portfolio"),
|
|
3173
|
+
annotations: annotationsFor("get_portfolio"),
|
|
3174
|
+
inputSchema: ORG_ID_INPUT
|
|
3175
|
+
},
|
|
3176
|
+
async (args) => {
|
|
3177
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3178
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/portfolio`));
|
|
3179
|
+
}
|
|
3180
|
+
);
|
|
3181
|
+
server.registerTool(
|
|
3182
|
+
"list_portfolio_findings",
|
|
3183
|
+
{
|
|
3184
|
+
title: titleOf("list_portfolio_findings"),
|
|
3185
|
+
description: descOf("list_portfolio_findings"),
|
|
3186
|
+
annotations: annotationsFor("list_portfolio_findings"),
|
|
3187
|
+
inputSchema: ORG_ID_INPUT
|
|
3188
|
+
},
|
|
3189
|
+
async (args) => {
|
|
3190
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3191
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/portfolio/findings`));
|
|
3192
|
+
}
|
|
3193
|
+
);
|
|
3194
|
+
server.registerTool(
|
|
3195
|
+
"get_recipe_drift",
|
|
3196
|
+
{
|
|
3197
|
+
title: titleOf("get_recipe_drift"),
|
|
3198
|
+
description: descOf("get_recipe_drift"),
|
|
3199
|
+
annotations: annotationsFor("get_recipe_drift"),
|
|
3200
|
+
inputSchema: z.object({
|
|
3201
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
|
|
3202
|
+
})
|
|
3203
|
+
},
|
|
3204
|
+
async (args) => {
|
|
3205
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3206
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/drift`));
|
|
3207
|
+
}
|
|
3208
|
+
);
|
|
3209
|
+
server.registerTool(
|
|
3210
|
+
"list_connectors",
|
|
3211
|
+
{
|
|
3212
|
+
title: titleOf("list_connectors"),
|
|
3213
|
+
description: descOf("list_connectors"),
|
|
3214
|
+
annotations: annotationsFor("list_connectors"),
|
|
3215
|
+
inputSchema: ORG_ID_INPUT
|
|
3216
|
+
},
|
|
3217
|
+
async (args) => {
|
|
3218
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3219
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/connectors`));
|
|
3220
|
+
}
|
|
3221
|
+
);
|
|
3222
|
+
const EDIT_INPUT = z.object({
|
|
3223
|
+
path: z.string().min(1).max(400).describe("Repo-relative path the recipe allows"),
|
|
3224
|
+
content: z.string().max(512 * 1024).describe("The full new file content"),
|
|
3225
|
+
reason: z.string().max(200).optional(),
|
|
3226
|
+
baseSha: z.string().min(1).max(80).nullable().optional().describe("The file's baseSha from the dry run (null = it did not exist). The confirm is refused if the file changed since, so it never reverts someone else's edit")
|
|
3227
|
+
});
|
|
3228
|
+
const ELEMENT_INPUT = z.enum(["design", "gates", "env", "routes", "store", "release"]).describe("Which part of the recipe the edit belongs to");
|
|
3229
|
+
server.registerTool(
|
|
3230
|
+
"propose_recipe_change",
|
|
3231
|
+
{
|
|
3232
|
+
title: titleOf("propose_recipe_change"),
|
|
3233
|
+
description: descOf("propose_recipe_change"),
|
|
3234
|
+
annotations: annotationsFor("propose_recipe_change"),
|
|
3235
|
+
inputSchema: z.object({
|
|
3236
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
|
|
3237
|
+
element: ELEMENT_INPUT,
|
|
3238
|
+
edits: z.array(EDIT_INPUT).min(1).max(30),
|
|
3239
|
+
title: z.string().min(1).max(120).optional(),
|
|
3240
|
+
confirm: z.boolean().optional().describe("true opens the draft PR; omitted or false is a dry run")
|
|
3241
|
+
})
|
|
3242
|
+
},
|
|
3243
|
+
async (args) => {
|
|
3244
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3245
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/changes`, {
|
|
3246
|
+
method: "POST",
|
|
3247
|
+
body: JSON.stringify({ element: args.element, edits: args.edits, title: args.title, dryRun: args.confirm !== true })
|
|
3248
|
+
}));
|
|
3249
|
+
}
|
|
3250
|
+
);
|
|
3251
|
+
server.registerTool(
|
|
3252
|
+
"propose_portfolio_change",
|
|
3253
|
+
{
|
|
3254
|
+
title: titleOf("propose_portfolio_change"),
|
|
3255
|
+
description: descOf("propose_portfolio_change"),
|
|
3256
|
+
annotations: annotationsFor("propose_portfolio_change"),
|
|
3257
|
+
inputSchema: z.object({
|
|
3258
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
|
|
3259
|
+
element: ELEMENT_INPUT,
|
|
3260
|
+
changes: z.array(z.object({ projectId: z.string().uuid(), edits: z.array(EDIT_INPUT).min(1).max(30) })).min(1).max(10),
|
|
3261
|
+
title: z.string().min(1).max(120).optional(),
|
|
3262
|
+
confirm: z.boolean().optional().describe("true opens one draft PR per repo; omitted or false is a dry run")
|
|
3263
|
+
})
|
|
3264
|
+
},
|
|
3265
|
+
async (args) => {
|
|
3266
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3267
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/portfolio/changes`, {
|
|
3268
|
+
method: "POST",
|
|
3269
|
+
body: JSON.stringify({ element: args.element, changes: args.changes, title: args.title, dryRun: args.confirm !== true })
|
|
3270
|
+
}));
|
|
3271
|
+
}
|
|
3272
|
+
);
|
|
3273
|
+
server.registerTool(
|
|
3274
|
+
"request_connector_action",
|
|
3275
|
+
{
|
|
3276
|
+
title: titleOf("request_connector_action"),
|
|
3277
|
+
description: descOf("request_connector_action"),
|
|
3278
|
+
annotations: annotationsFor("request_connector_action"),
|
|
3279
|
+
inputSchema: z.object({
|
|
3280
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
|
|
3281
|
+
connectorId: z.string().uuid().describe("Connector instance id (list_connectors)"),
|
|
3282
|
+
action: z.string().min(1).max(60).describe("For example set_rollout or promote_track"),
|
|
3283
|
+
payload: z.record(z.string(), z.unknown()).describe("The exact action input; the approval binds its hash"),
|
|
3284
|
+
projectId: z.string().uuid().optional(),
|
|
3285
|
+
reason: z.string().max(500).optional().describe("Why, shown to the approver")
|
|
3286
|
+
})
|
|
3287
|
+
},
|
|
3288
|
+
async (args) => {
|
|
3289
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3290
|
+
const body = { connectorId: args.connectorId, action: args.action, payload: args.payload, projectId: args.projectId, reason: args.reason };
|
|
3291
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/connector-actions`, { method: "POST", body: JSON.stringify(body) }));
|
|
3292
|
+
}
|
|
3293
|
+
);
|
|
3294
|
+
server.registerTool(
|
|
3295
|
+
"get_store_status",
|
|
3296
|
+
{
|
|
3297
|
+
title: titleOf("get_store_status"),
|
|
3298
|
+
description: descOf("get_store_status"),
|
|
3299
|
+
annotations: annotationsFor("get_store_status"),
|
|
3300
|
+
inputSchema: z.object({
|
|
3301
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
|
|
3302
|
+
})
|
|
3303
|
+
},
|
|
3304
|
+
async (args) => {
|
|
3305
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3306
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store`));
|
|
3307
|
+
}
|
|
3308
|
+
);
|
|
3309
|
+
server.registerTool(
|
|
3310
|
+
"get_radar",
|
|
3311
|
+
{
|
|
3312
|
+
title: titleOf("get_radar"),
|
|
3313
|
+
description: descOf("get_radar"),
|
|
3314
|
+
annotations: annotationsFor("get_radar"),
|
|
3315
|
+
inputSchema: z.object({
|
|
3316
|
+
scope: z.enum(["project", "organization"]).optional().describe("project (default) or organization"),
|
|
3317
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project (scope project)"),
|
|
3318
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization (scope organization)")
|
|
3319
|
+
})
|
|
3320
|
+
},
|
|
3321
|
+
async (args) => {
|
|
3322
|
+
if (args.scope === "organization") {
|
|
3323
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3324
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/radar`));
|
|
3325
|
+
}
|
|
3326
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3327
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/radar`));
|
|
3328
|
+
}
|
|
3329
|
+
);
|
|
3330
|
+
const PROJECT_ID_INPUT = z.object({
|
|
3331
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
|
|
3332
|
+
});
|
|
3333
|
+
server.registerTool(
|
|
3334
|
+
"run_radar",
|
|
3335
|
+
{
|
|
3336
|
+
title: titleOf("run_radar"),
|
|
3337
|
+
description: descOf("run_radar"),
|
|
3338
|
+
annotations: annotationsFor("run_radar"),
|
|
3339
|
+
inputSchema: PROJECT_ID_INPUT
|
|
3340
|
+
},
|
|
3341
|
+
async (args) => {
|
|
3342
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3343
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/radar/run`, { method: "POST" }));
|
|
3344
|
+
}
|
|
3345
|
+
);
|
|
3346
|
+
server.registerTool(
|
|
3347
|
+
"refresh_recipe",
|
|
3348
|
+
{
|
|
3349
|
+
title: titleOf("refresh_recipe"),
|
|
3350
|
+
description: descOf("refresh_recipe"),
|
|
3351
|
+
annotations: annotationsFor("refresh_recipe"),
|
|
3352
|
+
inputSchema: PROJECT_ID_INPUT
|
|
3353
|
+
},
|
|
3354
|
+
async (args) => {
|
|
3355
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3356
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/refresh`, { method: "POST" }));
|
|
3357
|
+
}
|
|
3358
|
+
);
|
|
3359
|
+
server.registerTool(
|
|
3360
|
+
"run_design_deviance",
|
|
3361
|
+
{
|
|
3362
|
+
title: titleOf("run_design_deviance"),
|
|
3363
|
+
description: descOf("run_design_deviance"),
|
|
3364
|
+
annotations: annotationsFor("run_design_deviance"),
|
|
3365
|
+
inputSchema: PROJECT_ID_INPUT
|
|
3366
|
+
},
|
|
3367
|
+
async (args) => {
|
|
3368
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3369
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/deviance/run`, { method: "POST" }));
|
|
3370
|
+
}
|
|
3371
|
+
);
|
|
3372
|
+
server.registerTool(
|
|
3373
|
+
"run_store_review",
|
|
3374
|
+
{
|
|
3375
|
+
title: titleOf("run_store_review"),
|
|
3376
|
+
description: descOf("run_store_review"),
|
|
3377
|
+
annotations: annotationsFor("run_store_review"),
|
|
3378
|
+
inputSchema: PROJECT_ID_INPUT
|
|
3379
|
+
},
|
|
3380
|
+
async (args) => {
|
|
3381
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3382
|
+
return jsonText(
|
|
3383
|
+
await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store/review`, { method: "POST" }, { minTimeoutMs: 9e4 })
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
);
|
|
3387
|
+
server.registerTool(
|
|
3388
|
+
"get_release_calendar",
|
|
3389
|
+
{
|
|
3390
|
+
title: titleOf("get_release_calendar"),
|
|
3391
|
+
description: descOf("get_release_calendar"),
|
|
3392
|
+
annotations: annotationsFor("get_release_calendar"),
|
|
3393
|
+
inputSchema: ORG_ID_INPUT
|
|
3394
|
+
},
|
|
3395
|
+
async (args) => {
|
|
3396
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3397
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/releases`));
|
|
3398
|
+
}
|
|
3399
|
+
);
|
|
3400
|
+
server.registerTool(
|
|
3401
|
+
"get_code_health",
|
|
3402
|
+
{
|
|
3403
|
+
title: titleOf("get_code_health"),
|
|
3404
|
+
description: descOf("get_code_health"),
|
|
3405
|
+
annotations: annotationsFor("get_code_health"),
|
|
3406
|
+
inputSchema: z.object({
|
|
3407
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
|
|
3408
|
+
days: z.number().int().min(1).max(365).optional().describe("Trend window in days (default 30)")
|
|
3409
|
+
})
|
|
3410
|
+
},
|
|
3411
|
+
async (args) => {
|
|
3412
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3413
|
+
const qs = new URLSearchParams({ project_id: pid, days: String(args.days ?? 30) });
|
|
3414
|
+
return jsonText(await apiCall(`/v1/admin/code-health?${qs}`));
|
|
3415
|
+
}
|
|
3416
|
+
);
|
|
3417
|
+
server.registerTool(
|
|
3418
|
+
"explain_finding",
|
|
3419
|
+
{
|
|
3420
|
+
title: titleOf("explain_finding"),
|
|
3421
|
+
description: descOf("explain_finding"),
|
|
3422
|
+
annotations: annotationsFor("explain_finding"),
|
|
3423
|
+
inputSchema: z.object({
|
|
3424
|
+
findingId: z.string().uuid().describe("Gate finding id (the id field of a finding in list_gate_findings, get_radar, get_code_health\u2026)")
|
|
3425
|
+
})
|
|
3426
|
+
},
|
|
3427
|
+
async (args) => {
|
|
3428
|
+
return jsonText(await apiCall(`/v1/admin/findings/${encodeURIComponent(args.findingId)}`));
|
|
3429
|
+
}
|
|
3430
|
+
);
|
|
3431
|
+
server.registerTool(
|
|
3432
|
+
"get_auto_release_status",
|
|
3433
|
+
{
|
|
3434
|
+
title: titleOf("get_auto_release_status"),
|
|
3435
|
+
description: descOf("get_auto_release_status"),
|
|
3436
|
+
annotations: annotationsFor("get_auto_release_status"),
|
|
3437
|
+
inputSchema: PROJECT_ID_INPUT
|
|
3438
|
+
},
|
|
3439
|
+
async (args) => {
|
|
3440
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3441
|
+
return jsonText(await apiCall(`/v1/admin/releases/auto-release?${new URLSearchParams({ project_id: pid })}`));
|
|
3442
|
+
}
|
|
3443
|
+
);
|
|
3444
|
+
server.registerTool(
|
|
3445
|
+
"get_design_settings",
|
|
3446
|
+
{
|
|
3447
|
+
title: titleOf("get_design_settings"),
|
|
3448
|
+
description: descOf("get_design_settings"),
|
|
3449
|
+
annotations: annotationsFor("get_design_settings"),
|
|
3450
|
+
inputSchema: PROJECT_ID_INPUT
|
|
3451
|
+
},
|
|
3452
|
+
async (args) => {
|
|
3453
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3454
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/settings`));
|
|
3455
|
+
}
|
|
3456
|
+
);
|
|
3457
|
+
server.registerTool(
|
|
3458
|
+
"set_design_settings",
|
|
3459
|
+
{
|
|
3460
|
+
title: titleOf("set_design_settings"),
|
|
3461
|
+
description: descOf("set_design_settings"),
|
|
3462
|
+
annotations: annotationsFor("set_design_settings"),
|
|
3463
|
+
inputSchema: z.object({
|
|
3464
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
|
|
3465
|
+
threshold: z.number().int().min(0).max(100).optional().describe("0\u2013100; the actions fire when the score is above it"),
|
|
3466
|
+
failCi: z.boolean().optional().describe("true: `mushi recipe check --push` fails CI above the threshold"),
|
|
3467
|
+
autofix: z.boolean().optional().describe("false turns the design auto-fix off; an API key cannot turn it on")
|
|
3468
|
+
})
|
|
3469
|
+
},
|
|
3470
|
+
async (args) => {
|
|
3471
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3472
|
+
const body = { threshold: args.threshold, failCi: args.failCi, autofix: args.autofix };
|
|
3473
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/settings`, { method: "PUT", body: JSON.stringify(body) }));
|
|
3474
|
+
}
|
|
3475
|
+
);
|
|
3476
|
+
server.registerTool(
|
|
3477
|
+
"get_recipe_sources",
|
|
3478
|
+
{
|
|
3479
|
+
title: titleOf("get_recipe_sources"),
|
|
3480
|
+
description: descOf("get_recipe_sources"),
|
|
3481
|
+
annotations: annotationsFor("get_recipe_sources"),
|
|
3482
|
+
inputSchema: z.object({
|
|
3483
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
|
|
3484
|
+
element: z.enum(["gates", "env", "routes"]).describe("gates (mushi.recipe.json budgets), env (.env.example) or routes (the inventory file)")
|
|
3485
|
+
})
|
|
3486
|
+
},
|
|
3487
|
+
async (args) => {
|
|
3488
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3489
|
+
const qs = new URLSearchParams({ element: args.element });
|
|
3490
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/sources?${qs}`));
|
|
3491
|
+
}
|
|
3492
|
+
);
|
|
3493
|
+
server.registerTool(
|
|
3494
|
+
"get_recipe_change",
|
|
3495
|
+
{
|
|
3496
|
+
title: titleOf("get_recipe_change"),
|
|
3497
|
+
description: descOf("get_recipe_change"),
|
|
3498
|
+
annotations: annotationsFor("get_recipe_change"),
|
|
3499
|
+
inputSchema: z.object({
|
|
3500
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
|
|
3501
|
+
jobId: z.string().uuid().describe("The jobId propose_recipe_change returned")
|
|
3502
|
+
})
|
|
3503
|
+
},
|
|
3504
|
+
async (args) => {
|
|
3505
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3506
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/changes/${encodeURIComponent(args.jobId)}`));
|
|
3507
|
+
}
|
|
3508
|
+
);
|
|
3509
|
+
server.registerTool(
|
|
3510
|
+
"get_store_reviews",
|
|
3511
|
+
{
|
|
3512
|
+
title: titleOf("get_store_reviews"),
|
|
3513
|
+
description: descOf("get_store_reviews"),
|
|
3514
|
+
annotations: annotationsFor("get_store_reviews"),
|
|
3515
|
+
inputSchema: PROJECT_ID_INPUT
|
|
3516
|
+
},
|
|
3517
|
+
async (args) => {
|
|
3518
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3519
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store/reviews`));
|
|
3520
|
+
}
|
|
3521
|
+
);
|
|
3522
|
+
server.registerTool(
|
|
3523
|
+
"pull_store_reviews",
|
|
3524
|
+
{
|
|
3525
|
+
title: titleOf("pull_store_reviews"),
|
|
3526
|
+
description: descOf("pull_store_reviews"),
|
|
3527
|
+
annotations: annotationsFor("pull_store_reviews"),
|
|
3528
|
+
inputSchema: PROJECT_ID_INPUT
|
|
3529
|
+
},
|
|
3530
|
+
async (args) => {
|
|
3531
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3532
|
+
return jsonText(
|
|
3533
|
+
await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store/reviews/pull`, { method: "POST" }, { minTimeoutMs: 9e4 })
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
);
|
|
3537
|
+
server.registerTool(
|
|
3538
|
+
"set_store_review_intake",
|
|
3539
|
+
{
|
|
3540
|
+
title: titleOf("set_store_review_intake"),
|
|
3541
|
+
description: descOf("set_store_review_intake"),
|
|
3542
|
+
annotations: annotationsFor("set_store_review_intake"),
|
|
3543
|
+
inputSchema: z.object({
|
|
3544
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
|
|
3545
|
+
enabled: z.boolean().describe("false turns intake off; true keeps it on (only the console can turn it on)"),
|
|
3546
|
+
maxRating: z.number().int().min(1).max(5).optional().describe("Reviews at or under this many stars are filed (1\u20135)")
|
|
3547
|
+
})
|
|
3548
|
+
},
|
|
3549
|
+
async (args) => {
|
|
3550
|
+
const pid = await resolveProjectId(args.projectId);
|
|
3551
|
+
const body = { enabled: args.enabled, maxRating: args.maxRating };
|
|
3552
|
+
return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store/reviews/settings`, { method: "PUT", body: JSON.stringify(body) }));
|
|
3553
|
+
}
|
|
3554
|
+
);
|
|
3555
|
+
const ACCOUNT_PROVIDER = z.enum(["apple", "google_play", "aws", "supabase", "vercel", "registrar", "stripe", "github", "cloudflare", "other"]);
|
|
3556
|
+
server.registerTool(
|
|
3557
|
+
"get_accounts_register",
|
|
3558
|
+
{
|
|
3559
|
+
title: titleOf("get_accounts_register"),
|
|
3560
|
+
description: descOf("get_accounts_register"),
|
|
3561
|
+
annotations: annotationsFor("get_accounts_register"),
|
|
3562
|
+
inputSchema: ORG_ID_INPUT
|
|
3563
|
+
},
|
|
3564
|
+
async (args) => {
|
|
3565
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3566
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/accounts`));
|
|
3567
|
+
}
|
|
3568
|
+
);
|
|
3569
|
+
server.registerTool(
|
|
3570
|
+
"save_register_account",
|
|
3571
|
+
{
|
|
3572
|
+
title: titleOf("save_register_account"),
|
|
3573
|
+
description: descOf("save_register_account"),
|
|
3574
|
+
annotations: annotationsFor("save_register_account"),
|
|
3575
|
+
inputSchema: z.object({
|
|
3576
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
|
|
3577
|
+
id: z.string().uuid().optional().describe("Account id to change (get_accounts_register); omit to record a new account"),
|
|
3578
|
+
provider: ACCOUNT_PROVIDER.optional().describe("Required when recording a new account"),
|
|
3579
|
+
displayName: z.string().min(1).max(120).optional().describe("Required when recording a new account"),
|
|
3580
|
+
ownerEmail: z.string().email().max(254).nullable().optional(),
|
|
3581
|
+
twoFactorDeclared: z.boolean().nullable().optional(),
|
|
3582
|
+
recoveryContact: z.string().max(200).nullable().optional().describe("A person who can recover the account (never a recovery code)"),
|
|
3583
|
+
adminCount: z.number().int().min(1).max(100).optional(),
|
|
3584
|
+
autoRenew: z.boolean().nullable().optional()
|
|
3585
|
+
})
|
|
3586
|
+
},
|
|
3587
|
+
async (args) => {
|
|
3588
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3589
|
+
const { organizationId: _org, id, ...fields } = args;
|
|
3590
|
+
if (!id && (!fields.provider || !fields.displayName)) {
|
|
3591
|
+
throw new MushiApiError(400, "VALIDATION_ERROR", "provider and displayName are required to record a new account (pass id to change one).");
|
|
3592
|
+
}
|
|
3593
|
+
const path = id ? `/v1/admin/orgs/${org}/accounts/${encodeURIComponent(id)}` : `/v1/admin/orgs/${org}/accounts`;
|
|
3594
|
+
return jsonText(await apiCall(path, { method: id ? "PATCH" : "POST", body: JSON.stringify(fields) }));
|
|
3595
|
+
}
|
|
3596
|
+
);
|
|
3597
|
+
server.registerTool(
|
|
3598
|
+
"remove_register_account",
|
|
3599
|
+
{
|
|
3600
|
+
title: titleOf("remove_register_account"),
|
|
3601
|
+
description: descOf("remove_register_account"),
|
|
3602
|
+
annotations: annotationsFor("remove_register_account"),
|
|
3603
|
+
inputSchema: z.object({
|
|
3604
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
|
|
3605
|
+
id: z.string().uuid().describe("Account id (get_accounts_register)")
|
|
3606
|
+
})
|
|
3607
|
+
},
|
|
3608
|
+
async (args) => {
|
|
3609
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3610
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/accounts/${encodeURIComponent(args.id)}`, { method: "DELETE" }));
|
|
3611
|
+
}
|
|
3612
|
+
);
|
|
3613
|
+
server.registerTool(
|
|
3614
|
+
"set_domain_auto_renew",
|
|
3615
|
+
{
|
|
3616
|
+
title: titleOf("set_domain_auto_renew"),
|
|
3617
|
+
description: descOf("set_domain_auto_renew"),
|
|
3618
|
+
annotations: annotationsFor("set_domain_auto_renew"),
|
|
3619
|
+
inputSchema: z.object({
|
|
3620
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
|
|
3621
|
+
domainId: z.string().uuid().describe("Domain id (the id of a domain in get_accounts_register)"),
|
|
3622
|
+
autoRenew: z.boolean().nullable().describe("true, false, or null for not known")
|
|
3623
|
+
})
|
|
3624
|
+
},
|
|
3625
|
+
async (args) => {
|
|
3626
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3627
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/domains/${encodeURIComponent(args.domainId)}`, {
|
|
3628
|
+
method: "PATCH",
|
|
3629
|
+
body: JSON.stringify({ autoRenew: args.autoRenew })
|
|
3630
|
+
}));
|
|
3631
|
+
}
|
|
3632
|
+
);
|
|
3633
|
+
server.registerTool(
|
|
3634
|
+
"get_spend_ledger",
|
|
3635
|
+
{
|
|
3636
|
+
title: titleOf("get_spend_ledger"),
|
|
3637
|
+
description: descOf("get_spend_ledger"),
|
|
3638
|
+
annotations: annotationsFor("get_spend_ledger"),
|
|
3639
|
+
inputSchema: ORG_ID_INPUT
|
|
3640
|
+
},
|
|
3641
|
+
async (args) => {
|
|
3642
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3643
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/spend`));
|
|
3644
|
+
}
|
|
3645
|
+
);
|
|
3646
|
+
server.registerTool(
|
|
3647
|
+
"import_spend_bill",
|
|
3648
|
+
{
|
|
3649
|
+
title: titleOf("import_spend_bill"),
|
|
3650
|
+
description: descOf("import_spend_bill"),
|
|
3651
|
+
annotations: annotationsFor("import_spend_bill"),
|
|
3652
|
+
inputSchema: z.object({
|
|
3653
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
|
|
3654
|
+
vendor: z.enum(["vercel", "aws", "supabase", "other"]),
|
|
3655
|
+
csv: z.string().min(1).describe("The bill CSV content (at most 5 MB)"),
|
|
3656
|
+
projectId: z.string().uuid().optional().describe("Put every row on this app; omit to match an app column by name or slug"),
|
|
3657
|
+
filename: z.string().max(200).optional()
|
|
3658
|
+
})
|
|
3659
|
+
},
|
|
3660
|
+
async (args) => {
|
|
3661
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3662
|
+
const body = { vendor: args.vendor, csv: args.csv, projectId: args.projectId, filename: args.filename };
|
|
3663
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/spend/imports`, { method: "POST", body: JSON.stringify(body) }, { minTimeoutMs: 6e4 }));
|
|
3664
|
+
}
|
|
3665
|
+
);
|
|
3666
|
+
server.registerTool(
|
|
3667
|
+
"remove_spend_import",
|
|
3668
|
+
{
|
|
3669
|
+
title: titleOf("remove_spend_import"),
|
|
3670
|
+
description: descOf("remove_spend_import"),
|
|
3671
|
+
annotations: annotationsFor("remove_spend_import"),
|
|
3672
|
+
inputSchema: z.object({
|
|
3673
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
|
|
3674
|
+
importId: z.string().uuid().describe("Import id (get_spend_ledger imports)")
|
|
3675
|
+
})
|
|
3676
|
+
},
|
|
3677
|
+
async (args) => {
|
|
3678
|
+
const org = encodeURIComponent(args.organizationId ?? "current");
|
|
3679
|
+
return jsonText(await apiCall(`/v1/admin/orgs/${org}/spend/imports/${encodeURIComponent(args.importId)}`, { method: "DELETE" }));
|
|
3680
|
+
}
|
|
3681
|
+
);
|
|
3682
|
+
server.registerTool(
|
|
3683
|
+
"import_portfolio_resources",
|
|
3684
|
+
{
|
|
3685
|
+
title: titleOf("import_portfolio_resources"),
|
|
3686
|
+
description: descOf("import_portfolio_resources"),
|
|
3687
|
+
annotations: annotationsFor("import_portfolio_resources"),
|
|
3688
|
+
inputSchema: z.object({
|
|
3689
|
+
organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
|
|
3690
|
+
csv: z.string().min(1).max(256 * 1024).describe("CSV with the columns kind, external_id, project and optionally role")
|
|
3691
|
+
})
|
|
3692
|
+
},
|
|
3693
|
+
async (args) => {
|
|
3694
|
+
const body = { organizationId: args.organizationId ?? "current", csv: args.csv };
|
|
3695
|
+
return jsonText(await apiCall("/v1/ingest/recipe/csv", { method: "POST", body: JSON.stringify(body) }));
|
|
3696
|
+
}
|
|
3697
|
+
);
|
|
3001
3698
|
server.registerTool(
|
|
3002
3699
|
"suggest_fix",
|
|
3003
3700
|
{
|
|
@@ -3710,6 +4407,18 @@ function createMushiServer(config) {
|
|
|
3710
4407
|
args: { mode: "dispatch" }
|
|
3711
4408
|
});
|
|
3712
4409
|
}
|
|
4410
|
+
const waiting = reports.filter(
|
|
4411
|
+
(r) => typeof r.last_reporter_reply_at === "string" && (typeof r.admin_seen_at !== "string" || Date.parse(r.last_reporter_reply_at) > Date.parse(r.admin_seen_at))
|
|
4412
|
+
);
|
|
4413
|
+
if (waiting.length > 0) {
|
|
4414
|
+
steps.push({
|
|
4415
|
+
priority: priority++,
|
|
4416
|
+
action: `Answer ${waiting.length} reporter${waiting.length === 1 ? "" : "s"} waiting for a reply`,
|
|
4417
|
+
reason: `They replied in your app's "Your reports" thread. Latest: ${label(waiting[0])}. Read it, then answer with reply_to_reporter.`,
|
|
4418
|
+
tool: "get_report_timeline",
|
|
4419
|
+
args: { reportId: waiting[0].id }
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
3713
4422
|
const fixing = reports.filter((r) => r.status === "fixing");
|
|
3714
4423
|
for (const r of fixing.slice(0, 2)) {
|
|
3715
4424
|
steps.push({
|
|
@@ -3745,7 +4454,7 @@ function createMushiServer(config) {
|
|
|
3745
4454
|
args: { status: "classified" }
|
|
3746
4455
|
});
|
|
3747
4456
|
}
|
|
3748
|
-
const summary = steps.length === 0 ? "Inbox is clear \u2014 no blocked fixes, no open user reports, no chores. Nothing needs your attention." : `${steps.length} prioritised step${steps.length === 1 ? "" : "s"}: ` + steps.map((s) => s.action).join(" \u2192 ");
|
|
4457
|
+
const summary = steps.length === 0 ? "Inbox is clear \u2014 no blocked fixes, no reporters waiting, no open user reports, no chores. Nothing needs your attention." : `${steps.length} prioritised step${steps.length === 1 ? "" : "s"}: ` + steps.map((s) => s.action).join(" \u2192 ");
|
|
3749
4458
|
return jsonResult({ steps, summary });
|
|
3750
4459
|
}
|
|
3751
4460
|
);
|
|
@@ -3764,6 +4473,9 @@ function createMushiServer(config) {
|
|
|
3764
4473
|
),
|
|
3765
4474
|
inventoryActionNodeId: z.string().uuid().optional().describe(
|
|
3766
4475
|
"Optional inventory Action node UUID for spec-traceability (\xA72.10). When provided, the fix-worker embeds the expected_outcome contract in the LLM prompt and runs validateAgainstSpec before opening the PR."
|
|
4476
|
+
),
|
|
4477
|
+
targetRepoId: z.string().uuid().optional().describe(
|
|
4478
|
+
"Optional linked repo id (project_repos.id) to open the fix PR against, for a project with several repos. Omit for the project's primary repo. Another project's repo id is rejected with 400 TARGET_REPO_NOT_IN_PROJECT."
|
|
3767
4479
|
)
|
|
3768
4480
|
}),
|
|
3769
4481
|
outputSchema: z.object({
|
|
@@ -3794,6 +4506,7 @@ function createMushiServer(config) {
|
|
|
3794
4506
|
reportId: args.reportId,
|
|
3795
4507
|
agent: args.agent,
|
|
3796
4508
|
inventoryActionNodeId: args.inventoryActionNodeId,
|
|
4509
|
+
...args.targetRepoId ? { targetRepoId: args.targetRepoId } : {},
|
|
3797
4510
|
...pid ? { projectId: pid } : {}
|
|
3798
4511
|
})
|
|
3799
4512
|
});
|
|
@@ -3940,6 +4653,9 @@ function createMushiServer(config) {
|
|
|
3940
4653
|
issueIds: z.array(z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/)).max(10).optional().describe('Sentry issue ids or short ids (e.g. "4501", "WEB-12"), at most 10'),
|
|
3941
4654
|
query: z.string().max(200).optional().describe("Sentry search query within the configured Sentry project (default is:unresolved). Not with issueIds."),
|
|
3942
4655
|
limit: z.number().int().min(1).max(10).optional().describe("How many issues a query imports (1-10, default 5)"),
|
|
4656
|
+
sinceDays: z.number().int().min(1).max(90).optional().describe("Only issues seen in the last N days (1-90). Search mode only."),
|
|
4657
|
+
cursor: z.string().regex(/^[0-9][0-9.:-]{0,63}$/).optional().describe("nextCursor from the previous import, to fetch the next page of a backlog. Search mode only."),
|
|
4658
|
+
sentryProject: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,49}$/).optional().describe("Which of the project's Sentry projects to search (default: the primary one). Search mode only."),
|
|
3943
4659
|
projectId: z.string().optional().describe("Project UUID. Defaults to configured project.")
|
|
3944
4660
|
}),
|
|
3945
4661
|
outputSchema: z.object({
|
|
@@ -3956,14 +4672,24 @@ function createMushiServer(config) {
|
|
|
3956
4672
|
created: z.array(z.string().nullable()).describe("Report ids created by this import"),
|
|
3957
4673
|
linked: z.array(z.string().nullable()).describe("Report ids that already existed for these issues"),
|
|
3958
4674
|
failed: z.number().describe("Issues that could not be imported"),
|
|
3959
|
-
indexing: z.looseObject({ queued: z.boolean(), paths: z.number() }).describe("Whether stack-frame files were queued for codebase indexing")
|
|
4675
|
+
indexing: z.looseObject({ queued: z.boolean(), paths: z.number() }).describe("Whether stack-frame files were queued for codebase indexing"),
|
|
4676
|
+
sentryProject: z.string().nullable().optional().describe("The Sentry project a search ran in (null for issueIds)"),
|
|
4677
|
+
sentryProjects: z.array(z.string()).optional().describe("This project's Sentry projects, primary first"),
|
|
4678
|
+
nextCursor: z.string().nullable().optional().describe("Pass back as cursor to import the next page; null when the backlog is done")
|
|
3960
4679
|
})
|
|
3961
4680
|
},
|
|
3962
4681
|
async (args) => {
|
|
3963
4682
|
const pid = await resolveProjectId(args.projectId);
|
|
3964
4683
|
const data = await apiCall(`/v1/admin/projects/${pid}/sentry/import`, {
|
|
3965
4684
|
method: "POST",
|
|
3966
|
-
body: JSON.stringify({
|
|
4685
|
+
body: JSON.stringify({
|
|
4686
|
+
issueIds: args.issueIds,
|
|
4687
|
+
query: args.query,
|
|
4688
|
+
limit: args.limit,
|
|
4689
|
+
sinceDays: args.sinceDays,
|
|
4690
|
+
cursor: args.cursor,
|
|
4691
|
+
sentryProject: args.sentryProject
|
|
4692
|
+
})
|
|
3967
4693
|
});
|
|
3968
4694
|
return jsonResult(data);
|
|
3969
4695
|
}
|
|
@@ -5248,6 +5974,66 @@ Be specific. No generic advice.`
|
|
|
5248
5974
|
return jsonText(data);
|
|
5249
5975
|
}
|
|
5250
5976
|
);
|
|
5977
|
+
server.registerTool(
|
|
5978
|
+
"get_repo_digest",
|
|
5979
|
+
{
|
|
5980
|
+
title: titleOf("get_repo_digest", CODEBASE_TOOL_CATALOG),
|
|
5981
|
+
description: descOf("get_repo_digest", CODEBASE_TOOL_CATALOG),
|
|
5982
|
+
annotations: annotationsFor("get_repo_digest", CODEBASE_TOOL_CATALOG),
|
|
5983
|
+
inputSchema: z.object({
|
|
5984
|
+
projectId: z.string().optional().describe("Project UUID (defaults to configured project)"),
|
|
5985
|
+
reportId: z.string().optional().describe("Scope to one bug: the report's files come first, then the rest of the repo by priority"),
|
|
5986
|
+
path: z.string().optional().describe('Scope to one folder, e.g. "apps/web/src"'),
|
|
5987
|
+
budgetTokens: z.number().int().min(2e3).max(2e5).optional().describe("Estimated token budget for the whole digest (default 50,000)"),
|
|
5988
|
+
include: z.array(z.string()).max(50).optional().describe('Glob patterns a file must match, e.g. ["src/**/*.ts", "*.md"]'),
|
|
5989
|
+
exclude: z.array(z.string()).max(50).optional().describe('Glob patterns to leave out, e.g. ["tests/"]'),
|
|
5990
|
+
ref: z.string().optional().describe("Branch, tag or commit SHA (default: the repo default branch)")
|
|
5991
|
+
})
|
|
5992
|
+
},
|
|
5993
|
+
async (args) => {
|
|
5994
|
+
const pid = args.projectId ?? projectId;
|
|
5995
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
|
|
5996
|
+
const qs = new URLSearchParams();
|
|
5997
|
+
if (args.budgetTokens) qs.set("budget", String(args.budgetTokens));
|
|
5998
|
+
if (args.reportId) qs.set("report_id", args.reportId);
|
|
5999
|
+
if (args.path) qs.set("path", args.path);
|
|
6000
|
+
if (args.include?.length) qs.set("include", args.include.join(","));
|
|
6001
|
+
if (args.exclude?.length) qs.set("exclude", args.exclude.join(","));
|
|
6002
|
+
if (args.ref) qs.set("ref", args.ref);
|
|
6003
|
+
const query = qs.toString();
|
|
6004
|
+
const data = await apiCall(
|
|
6005
|
+
`/v1/admin/projects/${pid}/codebase/digest${query ? `?${query}` : ""}`,
|
|
6006
|
+
void 0,
|
|
6007
|
+
// The server reads up to a few hundred files from GitHub (60 s deadline).
|
|
6008
|
+
{ minTimeoutMs: 9e4 }
|
|
6009
|
+
);
|
|
6010
|
+
return jsonText(data);
|
|
6011
|
+
}
|
|
6012
|
+
);
|
|
6013
|
+
server.registerTool(
|
|
6014
|
+
"get_repo_diagram",
|
|
6015
|
+
{
|
|
6016
|
+
title: titleOf("get_repo_diagram", CODEBASE_TOOL_CATALOG),
|
|
6017
|
+
description: descOf("get_repo_diagram", CODEBASE_TOOL_CATALOG),
|
|
6018
|
+
annotations: annotationsFor("get_repo_diagram", CODEBASE_TOOL_CATALOG),
|
|
6019
|
+
inputSchema: z.object({
|
|
6020
|
+
projectId: z.string().optional().describe("Project UUID (defaults to configured project)"),
|
|
6021
|
+
overlay: z.boolean().optional().describe("Also place the open bug reports and code findings on each part of the diagram")
|
|
6022
|
+
})
|
|
6023
|
+
},
|
|
6024
|
+
async (args) => {
|
|
6025
|
+
const pid = encodeURIComponent(await resolveProjectId(args.projectId));
|
|
6026
|
+
const data = await apiCall(
|
|
6027
|
+
`/v1/admin/projects/${pid}/codebase/diagram`
|
|
6028
|
+
);
|
|
6029
|
+
if (args.overlay !== true) return jsonText(data);
|
|
6030
|
+
const overlay = data.diagram ? await apiCall(`/v1/admin/projects/${pid}/codebase/diagram/overlay`, void 0, {
|
|
6031
|
+
// One GitHub tree read plus up to a few hundred reports.
|
|
6032
|
+
minTimeoutMs: 6e4
|
|
6033
|
+
}) : null;
|
|
6034
|
+
return jsonText({ ...data, overlay });
|
|
6035
|
+
}
|
|
6036
|
+
);
|
|
5251
6037
|
server.registerTool(
|
|
5252
6038
|
"use_mushi",
|
|
5253
6039
|
{
|