Compare commits

..

6 Commits

Author SHA1 Message Date
436ed59b41 merge(sdk): intègre feature/296-unified-agent-capabilities — metadata statiques mcpServers[].tools #296 (QA verte: npm run check) 2026-09-09 19:52:23 +02:00
d93e302320 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) 2026-09-09 19:52:23 +02:00
fdb3a907af merge(sdk): intègre feature/294-node-mcp-launch-docs — doc lancement serveurs MCP JS packagés #294 (QA verte: npm run check; vérifié contre hôte application/plugin/mod.rs) 2026-09-09 16:24:20 +02:00
b5a35563b6 docs(sdk): lancement des serveurs MCP JavaScript packagés — #294 (manifest: aucune substitution ${node}/${runtimeNode}/${hostNode} ni champ runtime, IdeA ne fournit pas de runtime Node plugin-scoped; command n'est pas du shell — démarrage direct avec args séparés, pas de fichiers de startup/alias/PATH du shell de login; command relatif résolu sous pluginRoot — command:"node" sans allowAbsoluteCommand devient ${pluginRoot}/node (cause ENOENT UnityPlugin #12); absolu littéral refusé sans allowAbsoluteCommand=true, command devenu absolu après substitution ${pluginRoot}/${appDataDir} passé tel quel; shebang #!/usr/bin/env node dépend du node visible du process app IdeA; recommandations: exécutable packagé incluant son runtime ou runtime géré sous stockage plugin/app pointé via ${pluginRoot}/${appDataDir}, sinon dépendance hôte explicite absolue /usr/bin/node avec allowAbsoluteCommand=true et scripts dans args; même règle pour command:"bash"; packaging-distribution: section Packaged Executables And Runtimes — bit exécutable et shebang valides dans le package installé, éviter le bare command:"node" en plugins distribués/multi-harness; services: distinction runCommand() (exécutable hôte de l'environnement task, aucune substitution) vs règles manifest mcpServers avec lien croisé; affirmations vérifiées par Git contre l'hôte crates/application/src/plugin/mod.rs:3199 substitution puis looks_absolute/allow_absolute_command sinon préfixage pluginRoot, et validation :3637 absolu littéral sans flag rejeté; QA verte reconfirmée par Git: npm run check — build, typecheck:examples, package:hello-plugin) 2026-09-09 16:24:16 +02:00
68fd612087 merge(sdk): intègre feature/293-mcp-tools-exposure-docs — doc contrat tools MCP plugins #293 (QA verte: npm run check) 2026-09-09 16:02:15 +02:00
42567a5305 docs(sdk): contrat d'exposition des tools MCP par les plugins — #293 (manifest: section MCP Servers And Agent Tools — contributes.mcpServers est le seul canal IdeA-native, aucun registerMcpTool/registerServer dans activate(ctx), règles id/displayName/transport stdio/autoStart requis pour la réconciliation MCP/command relatif résolu sous pluginRoot vs absolu refusé sans allowAbsoluteCommand/cwd défaut ${pluginRoot}, assignation par triplet pluginId/serverId/toolName devant matcher tools/list du serveur, example manifest aligné contributes.skills, guidance script packagé exécutable vs lancement via binaire hôte; activation-context: activate n'expose pas d'API d'enregistrement de tools MCP — ils viennent du process serveur stdio externe et de sa réponse tools/list, ctx.pluginRoot officialisé vs import.meta.url; project-plugin-assignments: assignation résolue vers serveurs manifestés autoStart uniquement, flow skill → serveur MCP → tools assignés sans chemins relatifs depuis le Markdown servi par idea_skill_read; QA verte reconfirmée par Git: npm run check — build, typecheck:examples, package:hello-plugin) 2026-09-09 16:02:05 +02:00
9 changed files with 231 additions and 2 deletions

View File

@ -45,12 +45,23 @@ focused project at invocation/render time.
The context never exposes internal IdeA gateways or Tauri commands. Use The context never exposes internal IdeA gateways or Tauri commands. Use
`ctx.services` and the registration APIs instead. `ctx.services` and the registration APIs instead.
`activate(ctx)` does not expose an API for registering MCP tools directly.
Agent-callable plugin tools are provided by stdio MCP servers declared in
`idea-plugin.json` under `contributes.mcpServers`. The plugin runtime can
register menu commands and layouts in-process, but MCP tools come from the
external server process and its MCP `tools/list` response.
`pluginRoot` identifies the committed package currently loaded by IdeA. It is `pluginRoot` identifies the committed package currently loaded by IdeA. It is
not the original source directory or archive path and may change after reinstall. not the original source directory or archive path and may change after reinstall.
Treat it as read-only, do not persist it, and resolve packaged scripts/assets Treat it as read-only, do not persist it, and resolve packaged scripts/assets
under it only while the plugin is active. Workspace services remain confined to under it only while the plugin is active. Workspace services remain confined to
project-owned paths. project-owned paths.
Use `ctx.pluginRoot` as the official way to resolve packaged scripts/assets from
runtime code. `import.meta.url` can still be useful inside a bundle for ordinary
module-relative JavaScript resolution, but it is not the host/plugin contract
for the installed package root.
## Disposal ## Disposal
Push every returned disposable to `ctx.subscriptions`: Push every returned disposable to `ctx.subscriptions`:

View File

@ -115,6 +115,7 @@ development loop.
"capabilities": ["ui", "tooling"], "capabilities": ["ui", "tooling"],
"activationScope": "app", "activationScope": "app",
"contributes": { "contributes": {
"skills": [],
"menus": [], "menus": [],
"menuItems": [], "menuItems": [],
"layouts": [], "layouts": [],
@ -247,6 +248,73 @@ Contribution ids are part of the runtime contract:
- `services.windows.open({ layoutType })` only accepts a layout type declared by - `services.windows.open({ layoutType })` only accepts a layout type declared by
the calling plugin. the calling plugin.
## MCP Servers And Agent Tools
`contributes.mcpServers` is the IdeA-native contract for plugin-provided
agent tools. A plugin does not register individual MCP tools from `activate(ctx)`.
Instead, the plugin ships or references a stdio MCP server process, declares it
in the manifest, and that server advertises its own tools through the MCP
`tools/list` protocol.
```json
{
"capabilities": ["mcp"],
"contributes": {
"mcpServers": [
{
"id": "unity-editor",
"displayName": "Unity Editor Tools",
"command": "scripts/unity-mcp-server.mjs",
"args": [],
"cwd": "${pluginRoot}",
"transport": "stdio",
"autoStart": true,
"tools": [
{
"name": "unity_get_scene",
"description": "Read the active Unity scene."
},
{
"name": "unity_run_tests"
}
]
}
]
}
}
```
MCP server contribution rules:
- `id` is stable within the plugin and is used by project/agent assignments.
- `displayName` is the human-readable server name shown in plugin settings.
- `transport` must currently be `"stdio"`.
- `autoStart: true` is required for IdeA to start the server during plugin MCP
reconciliation. Non-auto-start servers are manifest metadata only today.
- A relative `command` is resolved under the installed plugin package root.
- A literal absolute `command` is rejected unless `allowAbsoluteCommand: true`
is set. Use this only for an intentional dependency on a host binary.
- 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
match a tool name advertised by the MCP server. IdeA does not currently provide
a runtime API such as `ctx.mcp.registerTool()` or `ctx.mcp.registerServer()`.
For multi-harness workflows, keep `contributes.skills` as instructions and
expose executable behavior through assigned MCP tools. Do not make skill
Markdown depend on filesystem paths relative to the Markdown returned by
`idea_skill_read`; that body is served as read-only content, not mounted as a
working directory. If a skill needs packaged scripts, put the script behind a
manifest-declared MCP server and tell the agent to call the assigned tool.
## MCP Server Paths ## MCP Server Paths
`contributes.mcpServers` entries are resolved by the host before starting a `contributes.mcpServers` entries are resolved by the host before starting a
@ -255,11 +323,57 @@ declared MCP server. In `command`, `args`, `env` and `cwd`, the host expands:
- `${pluginRoot}` to the installed plugin package root. - `${pluginRoot}` to the installed plugin package root.
- `${appDataDir}` to the host-owned application data directory. - `${appDataDir}` to the host-owned application data directory.
There is currently no `${node}`, `${runtimeNode}`, `${hostNode}` substitution
and no `runtime` field. IdeA does not provide a plugin-scoped Node.js runtime
for MCP servers.
This string substitution is limited to manifest-declared MCP server startup. This string substitution is limited to manifest-declared MCP server startup.
Runtime handlers receive the same installed package location separately as Runtime handlers receive the same installed package location separately as
`ctx.pluginRoot`; `ctx.services.tasks.runCommand()` does not perform placeholder `ctx.pluginRoot`; `ctx.services.tasks.runCommand()` does not perform placeholder
substitution, and workspace APIs remain project-confined. substitution, and workspace APIs remain project-confined.
`command` is not shell syntax. IdeA starts it directly and passes `args`
separately. Do not rely on shell startup files, shell aliases, or a user's login
shell `PATH`.
Prefer a packaged executable for plugin-owned MCP servers:
```json
{
"command": "scripts/unity-mcp-server",
"transport": "stdio",
"autoStart": true
}
```
Because the command is package-relative, IdeA resolves it to
`${pluginRoot}/scripts/unity-mcp-server`. If this file is a script, its shebang
and executable bit must be valid in the installed package. A JavaScript shebang
such as `#!/usr/bin/env node` still depends on a `node` binary visible to the
IdeA app process, not necessarily the user's interactive shell.
For JavaScript MCP servers that must not depend on the user's shell `PATH`, use
one of these approaches:
- Ship an executable server that includes its runtime, or install a managed
runtime under plugin/app-owned storage and point `command` at that executable
with `${pluginRoot}` or `${appDataDir}`.
- Declare an explicit host dependency with an absolute command such as
`/usr/bin/node`, set `allowAbsoluteCommand: true`, and keep plugin-owned
script paths in `args`.
Avoid `command: "node"` for a packaged MCP server. With
`allowAbsoluteCommand: false`, it is treated as package-relative and resolves to
`${pluginRoot}/node`. With `allowAbsoluteCommand: true`, it is passed as a host
command name and depends on the environment inherited by IdeA, which may differ
from an interactive shell.
The same rule applies to shell wrappers. `command: "bash"` resolves to
`${pluginRoot}/bash` unless absolute-command handling is explicitly enabled. Use
a package-relative wrapper executable, or use an absolute shell such as
`/bin/bash` with `allowAbsoluteCommand: true` when that host dependency is
intentional.
## Validation ## Validation
Use the SDK validator in tests or build tooling: Use the SDK validator in tests or build tooling:

View File

@ -43,6 +43,25 @@ IdeA resolves React/ReactDOM bare imports to the host instance. Other bare
dependencies are not host-resolved. Bundle or vendor third-party dependencies dependencies are not host-resolved. Bundle or vendor third-party dependencies
other than React/ReactDOM into package-relative files. other than React/ReactDOM into package-relative files.
## Packaged Executables And Runtimes
Manifest-declared MCP servers are launched from `contributes.mcpServers` by
starting the declared `command` directly. IdeA does not inject a shell and does
not provide a plugin-scoped Node.js runtime.
For a packaged MCP server, prefer a package-relative executable such as
`scripts/my-plugin-mcp-server`. The installed file must keep the executable bit
and, when it is a script, a valid shebang. A JavaScript server with
`#!/usr/bin/env node` still requires `node` in the environment inherited by the
IdeA app process; this is not guaranteed to match the user's interactive shell,
especially when Node is provided by shell startup tooling.
For reproducible JavaScript MCP servers, either ship/install a runtime under the
plugin package or app-owned storage and point the manifest at that executable,
or declare an explicit absolute host dependency with `allowAbsoluteCommand:
true`. Avoid relying on a bare `command: "node"` in distributed or multi-harness
plugins.
## TypeScript Build ## TypeScript Build
The hello plugin uses plain `tsc`: The hello plugin uses plain `tsc`:

View File

@ -69,6 +69,12 @@ An agent MCP tool reference contains:
- `toolName`: the exact tool name advertised by that MCP server. The UI accepts - `toolName`: the exact tool name advertised by that MCP server. The UI accepts
this name as text because a dynamic MCP tool catalogue is not yet available. this name as text because a dynamic MCP tool catalogue is not yet available.
IdeA resolves plugin MCP assignments to manifest-declared servers, not to
in-process handlers registered from `activate(ctx)`. A tool assignment is useful
only when the plugin is enabled, runtime-active, the referenced MCP server has
`autoStart: true`, and the server process advertises the named tool through MCP
`tools/list`.
Missing `plugins`, `agents`, `skills`, or `tools` arrays default to empty arrays Missing `plugins`, `agents`, `skills`, or `tools` arrays default to empty arrays
when IdeA reads the document. Prefer writing them explicitly for clarity and when IdeA reads the document. Prefer writing them explicitly for clarity and
forward-compatible review. forward-compatible review.
@ -152,3 +158,10 @@ These actions solve different problems:
When both package content and assignments changed, reload the plugin first, When both package content and assignments changed, reload the plugin first,
confirm it is enabled for the project, update the assignments, then relaunch the confirm it is enabled for the project, update the assignments, then relaunch the
agent. agent.
If a plugin skill describes a workflow backed by packaged scripts, prefer this
flow: ship the scripts in the plugin package, expose them through a
manifest-declared MCP server, assign the specific MCP tools to the target agent,
and have the skill instruct the agent to call those tools. Avoid relative paths
from the skill Markdown to packaged scripts; the Markdown served by
`idea_skill_read` is content, not a stable filesystem anchor.

View File

@ -42,7 +42,8 @@ path under the project root. The host does not implicitly resolve `command` or
`args` against the plugin package. Use the absolute `ctx.pluginRoot` supplied at `args` against the plugin package. Use the absolute `ctx.pluginRoot` supplied at
activation time to construct an explicit path to a packaged script, and pass activation time to construct an explicit path to a packaged script, and pass
that path as `command` or as an argument to its interpreter. Treat package files that path as `command` or as an argument to its interpreter. Treat package files
as read-only. as read-only. Unlike manifest-declared MCP servers, `runCommand()` does not
substitute `${pluginRoot}` or `${appDataDir}`.
```ts ```ts
const script = `${ctx.pluginRoot.replace(/[\\/]+$/, "")}/scripts/check.mjs`; const script = `${ctx.pluginRoot.replace(/[\\/]+$/, "")}/scripts/check.mjs`;
@ -56,6 +57,12 @@ await ctx.services.tasks.runCommand({
}); });
``` ```
This task example uses `node` as a host executable visible to the task
environment. That is not the contract for `contributes.mcpServers`: MCP server
`command` values are resolved by the plugin manifest rules documented in
[Manifest](manifest.md), where a bare `command: "node"` is package-relative
unless external host-command handling is explicitly enabled.
## Tooling ## Tooling
`services.tooling.diagnose()` checks executables, environment values and files `services.tooling.diagnose()` checks executables, environment values and files

View File

@ -18,10 +18,11 @@
], ],
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "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", "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", "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", "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": [ "keywords": [
"idea", "idea",

View 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")));

View File

@ -5,6 +5,7 @@ export type {
IdeAPluginEngineConstraints, IdeAPluginEngineConstraints,
IdeAPluginLayoutContribution, IdeAPluginLayoutContribution,
IdeAPluginMcpServerContribution, IdeAPluginMcpServerContribution,
IdeAPluginMcpToolMetadata,
IdeAPluginMenuItemContribution, IdeAPluginMenuItemContribution,
IdeAPluginSkillContribution, IdeAPluginSkillContribution,
IdeAPluginSkillKind, IdeAPluginSkillKind,

View File

@ -95,6 +95,15 @@ export interface IdeAPluginMcpServerContribution {
transport: "stdio"; transport: "stdio";
autoStart?: boolean; autoStart?: boolean;
allowAbsoluteCommand?: 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 = export type PluginManifestValidationResult =
@ -269,6 +278,22 @@ function validateContributes(value: unknown, errors: string[]): void {
optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`); optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`);
optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`); optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`);
optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`); 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);
});
}
}); });
} }