From d93e3023204ec01412f5f1e0be424dc42a3fbd81 Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 9 Sep 2026 19:52:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20metadata=20statiques=20contributes?= =?UTF-8?q?.mcpServers[].tools=20=E2=80=94=20#296=20(type=20IdeAPluginMcpT?= =?UTF-8?q?oolMetadata=20{name=20exact=20non=20vide,=20description=20optio?= =?UTF-8?q?nnelle=20non=20vide}=20export=C3=A9=20public;=20validation=20ma?= =?UTF-8?q?nifest:=20tools=20optionnel,=20noms=20uniques=20par=20serveur?= =?UTF-8?q?=20=E2=80=94=20metadata=20d'affichage=20pour=20l'UI=20d'attribu?= =?UTF-8?q?tion=20de=20capacit=C3=A9s,=20ne=20remplace=20ni=20ne=20cache?= =?UTF-8?q?=20MCP=20tools/list=20et=20n'enregistre=20rien;=20scripts/check?= =?UTF-8?q?-manifest-tools.mjs=20+=20npm=20run=20test:manifest=20int=C3=A9?= =?UTF-8?q?gr=C3=A9=20=C3=A0=20check:=20optionnel/valide/doublon=20rejet?= =?UTF-8?q?=C3=A9;=20doc=20manifest.md:=20exemple=20unity=5Fget=5Fscene/ru?= =?UTF-8?q?n=5Ftests=20+=20r=C3=A8gles;=20QA=20verte=20reconfirm=C3=A9e=20?= =?UTF-8?q?par=20Git:=20npm=20run=20check)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/manifest.md | 16 +++++++++++++- package.json | 3 ++- scripts/check-manifest-tools.mjs | 38 ++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/manifest.ts | 25 +++++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 scripts/check-manifest-tools.mjs diff --git a/docs/manifest.md b/docs/manifest.md index 657b736..daee634 100644 --- a/docs/manifest.md +++ b/docs/manifest.md @@ -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 diff --git a/package.json b/package.json index 5fde6fc..8f73668 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/check-manifest-tools.mjs b/scripts/check-manifest-tools.mjs new file mode 100644 index 0000000..b1ed4fa --- /dev/null +++ b/scripts/check-manifest-tools.mjs @@ -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"))); diff --git a/src/index.ts b/src/index.ts index 3ae4698..4304a60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export type { IdeAPluginEngineConstraints, IdeAPluginLayoutContribution, IdeAPluginMcpServerContribution, + IdeAPluginMcpToolMetadata, IdeAPluginMenuItemContribution, IdeAPluginSkillContribution, IdeAPluginSkillKind, diff --git a/src/manifest.ts b/src/manifest.ts index d576ce5..06ce975 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -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(); + 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); + }); + } }); }