Compare commits
6 Commits
e3a0f46f9d
...
436ed59b41
| Author | SHA1 | Date | |
|---|---|---|---|
| 436ed59b41 | |||
| d93e302320 | |||
| fdb3a907af | |||
| b5a35563b6 | |||
| 68fd612087 | |||
| 42567a5305 |
@ -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`:
|
||||||
|
|||||||
114
docs/manifest.md
114
docs/manifest.md
@ -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:
|
||||||
|
|||||||
@ -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`:
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
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,
|
IdeAPluginEngineConstraints,
|
||||||
IdeAPluginLayoutContribution,
|
IdeAPluginLayoutContribution,
|
||||||
IdeAPluginMcpServerContribution,
|
IdeAPluginMcpServerContribution,
|
||||||
|
IdeAPluginMcpToolMetadata,
|
||||||
IdeAPluginMenuItemContribution,
|
IdeAPluginMenuItemContribution,
|
||||||
IdeAPluginSkillContribution,
|
IdeAPluginSkillContribution,
|
||||||
IdeAPluginSkillKind,
|
IdeAPluginSkillKind,
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user