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); + }); + } }); }