Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/loopover-mcp/bin/loopover-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,7 @@ function registerStdioTool<TInput>(
inputSchema: overrides?.input ?? contract.input,
outputSchema: contract.output,
annotations: advertised.annotations,
_meta: { category: advertised.category },
},
wrapStdioToolHandler(name, () => telemetryState().enabled, handler as (...args: unknown[]) => Promise<unknown>),
);
Expand Down
1 change: 1 addition & 0 deletions packages/loopover-miner/bin/loopover-miner-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ function registerMinerTool<TInput extends z.ZodObject, TOutput extends z.ZodObje
// The schema OBJECTS, never their `.shape` -- see above.
inputSchema: contract.input,
outputSchema: contract.output,
_meta: { category: advertised.category },
},
handler,
);
Expand Down
4 changes: 4 additions & 0 deletions scripts/lib/validate-mcp/invariants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ListedTool = {
annotations?: { readOnlyHint?: boolean | undefined; destructiveHint?: boolean | undefined } | undefined;
inputSchema?: { type?: string; properties?: Record<string, unknown>; required?: string[] } | undefined;
outputSchema?: { type?: string } | undefined;
_meta?: { category?: string | undefined } | undefined;
};

/**
Expand Down Expand Up @@ -74,6 +75,9 @@ export function checkAdvertisedMetadata(expected: readonly McpToolDefinition[],
failures.push(`${tool.name} advertises ${hint}=${String(advertised.annotations?.[hint])}, registry says ${String(tool.annotations[hint])}`);
}
}
if (advertised._meta?.category !== tool.category) {
failures.push(`${tool.name} advertises _meta.category=${String(advertised._meta?.category)}, registry says ${String(tool.category)}`);
}
}
return failures;
}
Expand Down
31 changes: 31 additions & 0 deletions test/contract/validate-mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,37 @@ describe("MCP contract validator (#9520)", () => {
}
}, 180_000);

it("REGRESSION (#10038): the stdio and miner servers advertise _meta.category for every listed tool", async () => {
// registerStdioTool and registerMinerTool used to omit `_meta` entirely, so half of the stdio
// server's tools/list (its locally-registered tools, as opposed to the proxied ones that inherit
// the remote's `_meta`) and all of the miner's carried no category, even though checkAdvertisedMetadata
// above already re-runs on every surface and would have caught it once the field was modelled.
const categoryByName = new Map(listToolDefinitions().map((tool) => [tool.name, tool.category]));

const stdio = await import("../../packages/loopover-mcp/bin/loopover-mcp");
const stdioClient = await connect(stdio.server);
try {
const listed = (await stdioClient.listTools()).tools as unknown as ListedTool[];
expect(listed.length).toBeGreaterThan(0);
for (const tool of listed) {
expect(tool._meta?.category).toBe(categoryByName.get(tool.name));
}
} finally {
await stdioClient.close().catch(() => undefined);
}

const minerClient = await connect(createMinerMcpServer({}));
try {
const listed = (await minerClient.listTools()).tools as unknown as ListedTool[];
expect(listed.length).toBeGreaterThan(0);
for (const tool of listed) {
expect(tool._meta?.category).toBe(categoryByName.get(tool.name));
}
} finally {
await minerClient.close().catch(() => undefined);
}
}, 180_000);

it("REGRESSION: one tool name has ONE locality, which is what makes gateway collisions impossible", () => {
// #9526's gateway mounts every `remote` tool onto the stdio server, which serves the `local-git` ones.
// That is only safe because a NAME belongs to exactly one entry in the one registry — the same name
Expand Down
18 changes: 17 additions & 1 deletion test/unit/validate-mcp-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ describe("validate-mcp invariants", () => {

describe("advertised metadata matches the registry's projection (#9655)", () => {
const projected = (name: string, overrides: Partial<McpToolDefinition> = {}): McpToolDefinition =>
({ name, title: `${name} title`, description: `${name} description`, annotations: { readOnlyHint: true, destructiveHint: false }, ...overrides }) as McpToolDefinition;
({ name, title: `${name} title`, description: `${name} description`, annotations: { readOnlyHint: true, destructiveHint: false }, category: "utility", ...overrides }) as McpToolDefinition;
const advertised = (name: string) => ({
name,
title: `${name} title`,
description: `${name} description`,
annotations: { readOnlyHint: true, destructiveHint: false },
_meta: { category: "utility" },
});

it("passes when every advertised field is the projected one", () => {
Expand Down Expand Up @@ -83,6 +84,21 @@ describe("validate-mcp invariants", () => {
expect(checkAdvertisedMetadata([projected("a")], [{ name: "a", title: "a title", description: "a description" }])).toEqual([
"a advertises readOnlyHint=undefined, registry says true",
"a advertises destructiveHint=undefined, registry says false",
"a advertises _meta.category=undefined, registry says utility",
]);
});

it("reports a tool advertising no _meta at all (#10038)", () => {
// Stdio's locally-registered half and the miner server sent title/description/annotations but
// no `_meta`, so half a server's tools/list was uncategorised while the other half (proxied, or
// the remote server) was not.
const { _meta: _dropped, ...noMeta } = advertised("a");
expect(checkAdvertisedMetadata([projected("a")], [noMeta])).toEqual(["a advertises _meta.category=undefined, registry says utility"]);
});

it("reports a _meta.category that disagrees with the registry's (#10038)", () => {
expect(checkAdvertisedMetadata([projected("a")], [{ ...advertised("a"), _meta: { category: "admin" } }])).toEqual([
"a advertises _meta.category=admin, registry says utility",
]);
});

Expand Down
Loading