feat(sdk): metadata statiques contributes.mcpServers[].tools — #296 (type IdeAPluginMcpToolMetadata {name exact non vide, description optionnelle non vide} exporté public; validation manifest: tools optionnel, noms uniques par serveur — metadata d'affichage pour l'UI d'attribution de capacités, ne remplace ni ne cache MCP tools/list et n'enregistre rien; scripts/check-manifest-tools.mjs + npm run test:manifest intégré à check: optionnel/valide/doublon rejeté; doc manifest.md: exemple unity_get_scene/run_tests + règles; QA verte reconfirmée par Git: npm run check)
This commit is contained in:
@ -268,7 +268,16 @@ in the manifest, and that server advertises its own tools through the MCP
|
||||
"args": [],
|
||||
"cwd": "${pluginRoot}",
|
||||
"transport": "stdio",
|
||||
"autoStart": true
|
||||
"autoStart": true,
|
||||
"tools": [
|
||||
{
|
||||
"name": "unity_get_scene",
|
||||
"description": "Read the active Unity scene."
|
||||
},
|
||||
{
|
||||
"name": "unity_run_tests"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -288,6 +297,11 @@ MCP server contribution rules:
|
||||
- A `command` that becomes absolute after `${pluginRoot}` or `${appDataDir}`
|
||||
substitution is allowed and is passed through as substituted.
|
||||
- `cwd` defaults to `${pluginRoot}` when omitted.
|
||||
- `tools` is optional static display metadata for IdeA's capability-assignment
|
||||
UI. Each entry has an exact non-empty `name` and an optional non-empty
|
||||
`description`; names must be unique within the server.
|
||||
- This metadata does not replace or cache MCP `tools/list`, and declaring a tool
|
||||
here does not register it or prove that the running server exposes it.
|
||||
|
||||
The tools an agent can call are assigned with the triplet
|
||||
`pluginId`/`serverId`/`toolName` in project plugin settings. `toolName` must
|
||||
|
||||
@ -18,10 +18,11 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"test:manifest": "npm run build && node scripts/check-manifest-tools.mjs",
|
||||
"build:hello-plugin": "tsc -p examples/hello-plugin/tsconfig.build.json",
|
||||
"package:hello-plugin": "npm run build && npm run build:hello-plugin && node scripts/package-hello-plugin.mjs",
|
||||
"typecheck:examples": "tsc -p examples/hello-plugin/tsconfig.json --noEmit",
|
||||
"check": "npm run build && npm run typecheck:examples && npm run package:hello-plugin"
|
||||
"check": "npm run test:manifest && npm run typecheck:examples && npm run package:hello-plugin"
|
||||
},
|
||||
"keywords": [
|
||||
"idea",
|
||||
|
||||
38
scripts/check-manifest-tools.mjs
Normal file
38
scripts/check-manifest-tools.mjs
Normal file
@ -0,0 +1,38 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { validatePluginManifest } from "../dist/index.js";
|
||||
|
||||
const base = {
|
||||
ideaPluginManifestVersion: 1,
|
||||
id: "dev.example.tools",
|
||||
displayName: "Tools",
|
||||
version: "1.0.0",
|
||||
main: "dist/index.js",
|
||||
trustLevel: "full",
|
||||
capabilities: ["mcp"],
|
||||
contributes: {
|
||||
mcpServers: [
|
||||
{
|
||||
id: "server",
|
||||
displayName: "Server",
|
||||
command: "bin/server",
|
||||
transport: "stdio"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
assert.equal(validatePluginManifest(base).success, true, "tools metadata remains optional");
|
||||
|
||||
const withTools = structuredClone(base);
|
||||
withTools.contributes.mcpServers[0].tools = [
|
||||
{ name: "read_scene", description: "Read the active scene." },
|
||||
{ name: "run_tests" }
|
||||
];
|
||||
assert.equal(validatePluginManifest(withTools).success, true, "valid tools metadata is accepted");
|
||||
|
||||
const duplicate = structuredClone(withTools);
|
||||
duplicate.contributes.mcpServers[0].tools.push({ name: "read_scene" });
|
||||
const duplicateResult = validatePluginManifest(duplicate);
|
||||
assert.equal(duplicateResult.success, false, "duplicate tool metadata is rejected");
|
||||
assert.ok(duplicateResult.errors.some((error) => error.includes("must be unique")));
|
||||
@ -5,6 +5,7 @@ export type {
|
||||
IdeAPluginEngineConstraints,
|
||||
IdeAPluginLayoutContribution,
|
||||
IdeAPluginMcpServerContribution,
|
||||
IdeAPluginMcpToolMetadata,
|
||||
IdeAPluginMenuItemContribution,
|
||||
IdeAPluginSkillContribution,
|
||||
IdeAPluginSkillKind,
|
||||
|
||||
@ -95,6 +95,15 @@ export interface IdeAPluginMcpServerContribution {
|
||||
transport: "stdio";
|
||||
autoStart?: boolean;
|
||||
allowAbsoluteCommand?: boolean;
|
||||
/** Optional static UI metadata; runtime MCP tools/list remains authoritative. */
|
||||
tools?: IdeAPluginMcpToolMetadata[];
|
||||
}
|
||||
|
||||
export interface IdeAPluginMcpToolMetadata {
|
||||
/** Exact tool name advertised by the MCP server. */
|
||||
name: string;
|
||||
/** Optional human-readable summary for capability-management UI. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type PluginManifestValidationResult =
|
||||
@ -269,6 +278,22 @@ function validateContributes(value: unknown, errors: string[]): void {
|
||||
optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`);
|
||||
optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`);
|
||||
optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`);
|
||||
validateArray(server, "tools", errors, (tool, toolIndex) => {
|
||||
requireString(tool, "name", errors, `contributes.mcpServers[${index}].tools[${toolIndex}].name`);
|
||||
if (tool.description !== undefined) {
|
||||
requireString(tool, "description", errors, `contributes.mcpServers[${index}].tools[${toolIndex}].description`);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(server.tools)) {
|
||||
const names = new Set<string>();
|
||||
server.tools.forEach((tool, toolIndex) => {
|
||||
if (!isRecord(tool) || typeof tool.name !== "string" || tool.name.trim() === "") return;
|
||||
if (names.has(tool.name)) {
|
||||
errors.push(`contributes.mcpServers[${index}].tools[${toolIndex}].name must be unique within the server`);
|
||||
}
|
||||
names.add(tool.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user