Superpowers/Specs/2026 07 14 Mcp Json Stdio Design
MCP JSON Configuration and stdio Design
Goal
Replace the bespoke MCP name-and-URL form with a JSON-first configuration editor and add local stdio MCP support through the official Rust MCP SDK. Preserve the existing AI SDK tool approval flow, raw tool schema display, and LazyStore persistence outside project .prg files.
Supported configuration formats
Project Graph uses the MCP Inspector-style mcpServers document as its display and export format:
{
"mcpServers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "C:/Projects"]
},
"context7": {
"type": "streamable-http",
"url": "https://mcp.context7.com/mcp"
}
}
}The importer also accepts the VS Code servers wrapper. A document must contain exactly one of mcpServers or servers; unwrapped single-server snippets are rejected. For compatibility, a server with command and no type is inferred as stdio, while a server with url and no type is inferred as streamable-http. In VS Code documents, type: "http" maps to the internal streamable-http transport.
The first implementation supports:
stdio:command, optionalargs,cwd, andenv.streamable-http:urland optional stringheaders.
It recognizes sse so it can return a precise unsupported-transport error, but it does not save or run SSE definitions until a dedicated runtime is added.
Configurations with both command and url, unsupported fields that affect execution, invalid URLs, embedded URL credentials, or normalized server-name collisions are rejected atomically. The editor never partially saves a document.
Normalized frontend model
Imported documents are converted into a host-independent model:
type AIMCPTransport =
| {
type: "stdio";
command: string;
args: string[];
cwd?: string;
env?: Record<string, string>;
}
| {
type: "streamable-http";
url: string;
headers?: Record<string, string>;
};
type AIMCPServerDefinition = {
name: string;
transport: AIMCPTransport;
};
type AIMCPServerRuntimeState = {
enabled: boolean;
enabledTools: string[];
cachedTools: AIMCPToolDescriptor[];
trustedDefinitionHash?: string;
};The JSON object key is the server identity; no UUID is introduced. The original name is retained for display. The existing readable normalized name is used only for model-facing tool names, and collisions after normalization are rejected.
Persistence and reconciliation
LazyStore("ai-mcp-servers.json") remains the only persistent location. The version 2 payload contains normalized definitions and runtime state separately. MCP configuration is global to the application and is never written into .prg files.
Saving JSON replaces the complete definition document. Runtime state is reconciled by server name:
- An unchanged definition retains its enabled state, selected tools, cached schemas, and process trust.
- A new or changed definition is disabled and loses cached tools, selected tools, and process trust.
- A removed definition loses its runtime state and any running stdio process is stopped.
The application is unreleased, so version 1 store data is not migrated.
Remote HTTP runtime
Remote servers continue to use @ai-sdk/mcp. The internal streamable-http transport maps to AI SDK type: "http", using the existing Tauri HTTP fetch implementation. Literal headers are passed only to the transport and never included in model prompts, tool descriptions, or error messages.
HTTP clients retain the existing request-scoped lifecycle and are closed on completion, abort, setup failure, or streaming error.
Local stdio runtime
The Tauri backend uses the official rmcp Rust SDK with its child-process transport. It owns an application-level map of running stdio clients keyed by server name and exposes focused commands to the frontend:
- start or replace a trusted stdio server and return its tool descriptors;
- list tools for a running server;
- invoke a named tool with JSON arguments;
- stop one server;
- stop servers that no longer exist in the saved configuration.
The frontend does not receive raw stdin/stdout handles and does not implement MCP JSON-RPC framing. It creates AI SDK dynamic tools from the Rust-returned descriptors. Each dynamic tool has needsApproval: true, and its execute function invokes the Rust call_tool command.
stdio processes are application-scoped so they can be reused across chat sessions. They stop when disabled, deleted, replaced, or when the application exits. An unexpected child exit is surfaced as an offline error; the first version does not automatically restart in a loop.
Trust and approval
Local process trust and model tool approval are independent:
- Before starting an untrusted or changed stdio definition, Project Graph shows the exact executable, arguments, working directory, and environment variable names. Environment values are not repeated in the dialog. The server starts only after explicit confirmation.
- Every model-initiated MCP tool invocation continues to require the existing AI SDK approval response.
The trusted value is a deterministic fingerprint of the normalized stdio definition. Changing any execution-affecting field invalidates trust. Saving configuration never starts a process or performs a network request.
Literal env and headers values are stored locally in the Tauri Store. The UI warns that these values are stored in the local configuration file. ${input:...} and OAuth are outside this implementation because they require a separate secure credential-provider design.
Management UI
The MCP section replaces the name-and-URL form with a monospaced JSON textarea initialized from the canonical mcpServers document. โValidate and saveโ parses and validates the full document without connecting.
Below the editor, the existing server cards remain responsible for connection status, process trust, enabling the server, refreshing tools, selecting individual tools, deleting definitions, and displaying raw JSON Schema. stdio cards show the executable and arguments; HTTP cards show the URL. Newly imported or changed servers are disabled until tools are successfully discovered.
All parse, validation, persistence, spawn, handshake, discovery, tool-call, and shutdown failures are visible through the existing dialog or toast UI. Failures are not silently logged and do not partially update persisted state.
Testing
TypeScript unit tests cover both wrappers, inference, VS Code transport aliases, invalid and conflicting definitions, normalized-name collisions, deterministic serialization, state reconciliation, dynamic stdio tool construction, approval decoration, and runtime cleanup.
Rust unit tests cover definition validation and command construction without spawning arbitrary real programs. Rust integration behavior is exercised through a test MCP child fixture or the SDK's testable client boundary, including tool discovery, calls, duplicate start replacement, stop, and missing-client errors.
Verification includes focused Vitest tests, Rust tests, TypeScript type checking, the existing test suite, Rust cargo check, and git diff --check.
Superpowers/Plans/2026 07 14 Ai Sdk Mcp Skills
AI SDK MCP, Tool Approval, and Agent Skills Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add remote Streamable HTTP MCP tools with AI SDK approval and session-persistent Agent Skills to Project Graph's existing AI chat.
Architecture: Keep AITools as the built-in tool source. Add focused MCP and Skills modules, then compose their tools and system context inside AIEngine for each chat request. Persist MCP configuration and project-skill trust with LazyStore, while activated skill snapshots remain part of the existing chat-session record.
Tech Stack: TypeScript, React, AI SDK 6, @ai-sdk/mcp, Tauri HTTP/FS/Store plugins, Zod 4, YAML, Vitest.
Task 1: Install MCP dependencies and define MCP configuration behavior
Files:
Modify:
app/package.jsonModify:
pnpm-lock.yamlCreate:
app/src/core/service/dataManageService/aiEngine/AIMCP.test.tsCreate:
app/src/core/service/dataManageService/aiEngine/AIMCP.tsStep 1: Add failing tests for server-name normalization, tool namespacing, filtering, and approval decoration
expect(normalizeMCPServerName(" Context 7 ")).toBe("context-7");
expect(createMCPToolName("context-7", "resolve_library")).toBe("mcp__context-7__resolve_library");
expect(decorateMCPTools("context-7", tools, ["resolve_library"])).toMatchObject({
"mcp__context-7__resolve_library": { needsApproval: true },
});- Step 2: Run the MCP test and verify it fails because
AIMCPdoes not exist
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts
- Step 3: Install the AI SDK 6-compatible
@ai-sdk/mcp1.x line
Run: pnpm --filter @graphif/project-graph add @ai-sdk/mcp@^1.0.61
- Step 4: Implement configuration validation,
LazyStore("ai-mcp-servers.json"), tool filtering, stable namespacing, andneedsApproval: truedecoration
export type AIMCPServerConfig = {
name: string;
url: string;
enabled: boolean;
enabledTools: string[];
cachedTools: AIMCPToolDescriptor[];
};- Step 5: Run the MCP test and verify it passes
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts
Task 2: Connect MCP tools to each AI chat request
Files:
Modify:
app/src/core/service/dataManageService/aiEngine/AIMCP.tsModify:
app/src/core/service/dataManageService/aiEngine/AIEngine.tsxTest:
app/src/core/service/dataManageService/aiEngine/AIMCP.test.tsStep 1: Add a failing lifecycle test that closes every successfully created client when a later connection fails
await expect(prepareMCPTools(configs, fakeFactory)).rejects.toThrow("offline");
expect(firstClient.close).toHaveBeenCalledOnce();Step 2: Run the test and verify the missing lifecycle behavior fails
Step 3: Implement Streamable HTTP client creation through
createMCPClient, discover tools withclient.tools(), and return an idempotentclose()callback
export type PreparedMCPTools = {
tools: ToolSet;
descriptors: AIMCPToolDescriptor[];
close(): Promise<void>;
};Step 4: Merge MCP tools into
AIEngineand close clients on setup failure, stream completion, and abortStep 5: Run the MCP tests and verify they pass
Task 3: Render and submit native AI SDK approval requests
Files:
Create:
app/src/core/service/dataManageService/aiEngine/AIToolUIPart.test.tsCreate:
app/src/core/service/dataManageService/aiEngine/AIToolUIPart.tsModify:
app/src/sub/AIWindow.tsxStep 1: Add failing tests for static and dynamic tool-part detection and display names
expect(isAIToolPart({ type: "tool-delete_node" })).toBe(true);
expect(isAIToolPart({ type: "dynamic-tool", toolName: "mcp__demo__remove" })).toBe(true);
expect(getAIToolPartName({ type: "dynamic-tool", toolName: "mcp__demo__remove" })).toBe("mcp__demo__remove");Step 2: Run the test and verify it fails because the helper does not exist
Step 3: Implement the pure tool-part helpers
Step 4: Update
useChatto exposeaddToolApprovalResponse, configurelastAssistantMessageIsCompleteWithApprovalResponses, and pass the approval callback into message renderingStep 5: Render
approval-requestedwith existing shadcnButtoncomponents and calladdToolApprovalResponse({ id, approved })Step 6: Run the helper test and verify it passes
Task 4: Discover and activate Agent Skills safely
Files:
Create:
app/src/core/service/dataManageService/aiEngine/AISkills.test.tsCreate:
app/src/core/service/dataManageService/aiEngine/AISkills.tsStep 1: Add failing tests for SKILL.md parsing, project-over-user precedence, invalid frontmatter, resource traversal, and activation deduplication
expect(parseSkillMarkdown(validSkill, location).name).toBe("create-setting-item");
expect(mergeSkillCatalogs(userSkills, projectSkills).get("shared")?.scope).toBe("project");
expect(resolveSkillResourcePath(baseDir, "../secret.txt")).toThrow();Step 2: Run the Skills test and verify it fails because
AISkillsdoes not existStep 3: Implement YAML frontmatter parsing with the existing
yamldependency and deterministic catalog mergingStep 4: Implement user/project
.agents/skills/*/SKILL.mddiscovery through Tauri FS, with project skills disabled until trustedStep 5: Implement
activate_skillandread_skill_resourceAI SDK tools with enum-constrained names, 256 KiB UTF-8 limits, and root containment checksStep 6: Run the Skills test and verify it passes
Task 5: Persist activated skill snapshots in each chat session
Files:
Modify:
app/src/core/service/dataManageService/aiEngine/AIChatSessionStore.tsModify:
app/src/core/service/dataManageService/aiEngine/AIEngine.tsxCreate:
app/src/core/service/dataManageService/aiEngine/AISkillSession.test.tsCreate:
app/src/core/service/dataManageService/aiEngine/AISkillSession.tsStep 1: Add a failing test that builds system context from the current catalog and activated snapshots without duplicate bodies
expect(buildSkillSystemContext(catalog, [snapshot, snapshot])).toContain("<skill_content");
expect(buildSkillSystemContext(catalog, [snapshot, snapshot]).match(/instructions/g)).toHaveLength(1);Step 2: Run the test and verify it fails because session skill context is not implemented
Step 3: Extend stored sessions with validated
activatedSkillssnapshots and add get/activate methods that preserve existing messages and memoryStep 4: Build the system prompt as base instructions, available-skills catalog, active skill snapshots, then compressed session memory
Step 5: Run session and Skills tests and verify they pass
Task 6: Add MCP management and dynamic schema display
Files:
Modify:
app/src/sub/AIToolsWindow.tsxStep 1: Use installed shadcn
Button,Input,Switch,Collapsible, andBadgecomponents to list, add, test, enable, and delete unauthenticated HTTP MCP serversStep 2: On connection test, discover tools, persist cached descriptors, and let users enable individual tools
Step 3: Extend the AI tools window to group built-in tools, MCP tools, and Skills tools and render raw JSON Schema without assuming Zod
Step 4: Surface all configuration and discovery failures through visible dialogs/toasts rather than console-only handling
Task 7: Verify the integrated feature
Files:
Test:
app/src/core/service/dataManageService/aiEngine/AIMCP.test.tsTest:
app/src/core/service/dataManageService/aiEngine/AIToolUIPart.test.tsTest:
app/src/core/service/dataManageService/aiEngine/AISkills.test.tsTest:
app/src/core/service/dataManageService/aiEngine/AISkillSession.test.tsStep 1: Run all newly added tests
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts app/src/core/service/dataManageService/aiEngine/AIToolUIPart.test.ts app/src/core/service/dataManageService/aiEngine/AISkills.test.ts app/src/core/service/dataManageService/aiEngine/AISkillSession.test.ts
- Step 2: Run the Project Graph type check and distinguish new diagnostics from the recorded baseline diagnostics
Run: pnpm --filter @graphif/project-graph type-check
- Step 3: Run the complete test suite and compare failures with the recorded three baseline import-resolution failures
Run: pnpm test --run
- Step 4: Review
git diff --checkand the final diff for unrelated changes and secret leakage
Superpowers/Plans/2026 07 14 Mcp Json Stdio
MCP JSON Configuration and stdio Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the MCP name-and-URL form with an ecosystem-compatible JSON configuration editor and run trusted local stdio MCP servers through the official Rust rmcp SDK.
Architecture: Parse mcpServers and VS Code servers documents into one normalized TypeScript model, persist definitions separately from runtime state in the existing Tauri Store, and keep remote HTTP servers on @ai-sdk/mcp. Add a Tauri-managed rmcp process manager for stdio servers, expose focused list/call/stop commands, and adapt returned schemas into approval-required AI SDK dynamic tools.
Tech Stack: TypeScript, React, Zod 4, AI SDK 6, @ai-sdk/mcp, Vitest, Tauri 2, Rust, rmcp 2.2, Tokio, LazyStore.
File map
- Create
app/src/core/service/dataManageService/aiEngine/AIMCPConfig.ts: JSON parsing, canonical serialization, normalized definitions, version 2 Store validation, state reconciliation, and trust fingerprints. - Create
app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts: parser, serializer, reconciliation, and validation tests. - Modify
app/src/core/service/dataManageService/aiEngine/AIMCP.ts: remote client mapping plus unified HTTP/stdio tool preparation. - Modify
app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts: HTTP header mapping, stdio tool adaptation, and lifecycle tests. - Create
app/src/core/service/dataManageService/aiEngine/AIMCPStdio.ts: typed Tauri command bridge and stdio AI SDK tool construction. - Create
app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.ts: command payload and approval decoration tests. - Create
app/src-tauri/src/cmd/mcp.rs: official Rust SDK process manager and Tauri commands. - Modify
app/src-tauri/src/cmd/mod.rs: export the MCP command module. - Modify
app/src-tauri/src/lib.rs: manage the process state and register commands. - Modify
app/src-tauri/Cargo.tomlandapp/src-tauri/Cargo.lock: addrmcpand required Tokio features. - Modify
app/src/sub/AIToolsWindow.tsx: JSON editor, trust confirmation, transport-specific server cards, and runtime updates. - Modify
app/src/core/service/dataManageService/aiEngine/AIEngine.tsx: load the version 2 snapshot and prepare both HTTP and stdio tools.
Task 1: Parse and normalize ecosystem MCP JSON
Files:
Create:
app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.tsCreate:
app/src/core/service/dataManageService/aiEngine/AIMCPConfig.tsStep 1: Write failing parser and serializer tests
/* Detailed source-code truncated for AI context efficiency. */- Step 2: Run the parser test and verify it fails because the module does not exist
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts
Expected: FAIL with an import-resolution error for ./AIMCPConfig.
- Step 3: Implement normalized types and strict parsing with the existing Zod dependency
import md5 from "md5";
import { z } from "zod/v4";
export type AIMCPStdioTransport = {
type: "stdio";
command: string;
args: string[];
cwd?: string;
env?: Record<string, string>;
};
export type AIMCPHttpTransport = {
type: "streamable-http";
url: string;
headers?: Record<string, string>;
};
export type AIMCPServerDefinition = {
name: string;
transport: AIMCPStdioTransport | AIMCPHttpTransport;
};
const stringRecord = z.record(z.string(), z.string());
const sourceServerSchema = z
.object({
type: z.enum(["stdio", "http", "streamable-http", "sse"]).optional(),
command: z.string().min(1).optional(),
args: z.array(z.string()).optional(),
cwd: z.string().min(1).optional(),
env: stringRecord.optional(),
url: z.string().min(1).optional(),
headers: stringRecord.optional(),
})
.strict();
export function createMCPDefinitionFingerprint(definition: AIMCPServerDefinition): string {
return md5(JSON.stringify(definition));
}Implement parseMCPConfigDocument so it validates exactly one wrapper, sorts entries by their document order, infers omitted transport types, rejects SSE with a precise error, validates HTTP(S) URLs without credentials, rejects stdio-only fields on HTTP entries and HTTP-only fields on stdio entries, and rejects collisions after normalizeMCPServerName.
Implement serializeMCPConfigDocument with mcpServers, explicit type, and two-space indentation.
- Step 4: Run the parser test and verify it passes
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts
Expected: PASS.
- Step 5: Commit the parser
git add app/src/core/service/dataManageService/aiEngine/AIMCPConfig.ts app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts
git commit -m "feat: parse ecosystem MCP configurations"Task 2: Separate persisted definitions from runtime state
Files:
Modify:
app/src/core/service/dataManageService/aiEngine/AIMCPConfig.tsModify:
app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.tsModify:
app/src/core/service/dataManageService/aiEngine/AIMCP.tsStep 1: Add failing reconciliation tests
it("preserves runtime state only while the definition is unchanged", () => {
const definition = {
name: "local",
transport: { type: "stdio" as const, command: "node", args: ["old.js"] },
};
const current: AIMCPSnapshot = {
version: 2,
definitions: [definition],
runtime: {
local: {
enabled: true,
enabledTools: ["read"],
cachedTools: [
{
serverName: "local",
name: "read",
modelName: "mcp__local__read",
inputSchema: { type: "object", properties: {} },
},
],
trustedDefinitionHash: "trusted",
},
},
};
expect(reconcileMCPDefinitions(current, [definition]).runtime.local.enabled).toBe(true);
expect(
reconcileMCPDefinitions(current, [
{ name: "local", transport: { type: "stdio", command: "node", args: ["new.js"] } },
]).runtime.local,
).toEqual({ enabled: false, enabledTools: [], cachedTools: [] });
});- Step 2: Run the test and verify the missing Store model fails
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts
Expected: FAIL because snapshot and reconciliation functions are not exported.
- Step 3: Implement the version 2 snapshot and Store API
export type AIMCPServerRuntimeState = {
enabled: boolean;
enabledTools: string[];
cachedTools: AIMCPToolDescriptor[];
trustedDefinitionHash?: string;
};
export type AIMCPSnapshot = {
version: 2;
definitions: AIMCPServerDefinition[];
runtime: Record<string, AIMCPServerRuntimeState>;
};
export namespace AIMCPStore {
export async function load(): Promise<AIMCPSnapshot>;
export async function saveDefinitions(definitions: AIMCPServerDefinition[]): Promise<AIMCPSnapshot>;
export async function updateRuntime(
serverName: string,
update: (state: AIMCPServerRuntimeState) => AIMCPServerRuntimeState,
): Promise<AIMCPSnapshot>;
}Keep LazyStore("ai-mcp-servers.json"), the existing initialization guard, and serialized write queue. Store one { version: 2, definitions, runtime } value under the existing servers key. Treat missing data as an empty version 2 snapshot and reject version 1 without migration.
Move AIMCPToolDescriptor into AIMCPConfig.ts so Store validation does not depend on runtime client creation. Export materializeMCPServers(snapshot) for runtime and UI consumers.
- Step 4: Run configuration and existing MCP tests
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts
Expected: new configuration tests PASS; existing MCP tests may still fail at old type call sites, which Task 5 updates.
- Step 5: Commit persisted-state separation
git add app/src/core/service/dataManageService/aiEngine/AIMCPConfig.ts app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts app/src/core/service/dataManageService/aiEngine/AIMCP.ts
git commit -m "refactor: separate MCP definitions and runtime state"Task 3: Add the Rust stdio MCP process manager
Files:
Modify:
app/src-tauri/Cargo.tomlModify:
app/src-tauri/Cargo.lockCreate:
app/src-tauri/src/cmd/mcp.rsModify:
app/src-tauri/src/cmd/mod.rsModify:
app/src-tauri/src/lib.rsStep 1: Add
rmcpand Tokio process support
Run from app/src-tauri:
cargo add [email protected] --no-default-features --features client,transport-child-processChange the direct Tokio dependency to:
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "process", "sync"] }- Step 2: Write failing Rust tests for validation and missing clients
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validates_stdio_definitions_without_a_shell() {
let config = McpStdioConfig {
server_name: "filesystem".into(),
command: "npx".into(),
args: vec!["-y".into(), "server".into()],
cwd: None,
env: HashMap::new(),
};
assert_eq!(validate_config(&config).unwrap().command, "npx");
assert!(validate_config(&McpStdioConfig { command: " ".into(), ..config }).is_err());
}
#[tokio::test]
async fn missing_clients_return_a_visible_error() {
let manager = McpStdioManager::default();
let error = manager.list_tools("missing").await.unwrap_err();
assert!(error.contains("not running"));
}
}- Step 3: Run the Rust test and verify it fails because the module is absent
Run: cargo test --manifest-path app/src-tauri/Cargo.toml cmd::mcp::tests
Expected: FAIL because cmd::mcp and its types do not exist.
- Step 4: Implement the focused
rmcpmanager
use rmcp::{
model::CallToolRequestParams,
service::{RoleClient, RunningService},
transport::{ConfigureCommandExt, TokioChildProcess},
ServiceExt,
};
use serde::Deserialize;
use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
use tokio::{process::Command, sync::Mutex};
type RunningClient = RunningService<RoleClient, ()>;
type SharedClient = Arc<Mutex<RunningClient>>;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpStdioConfig {
pub server_name: String,
pub command: String,
#[serde(default)]
pub args: Vec<String>,
pub cwd: Option<String>,
#[serde(default)]
pub env: HashMap<String, String>,
}
#[derive(Default)]
pub struct McpStdioManager {
clients: Mutex<HashMap<String, SharedClient>>,
}Implement manager methods that validate non-empty names and commands, build tokio::process::Command directly without invoking cmd /c, PowerShell, or /bin/sh, spawn with TokioChildProcess, perform the SDK handshake with ().serve(...), replace and gracefully close any previous client with the same name, serialize list_all_tools() to JSON, call tools with CallToolRequestParams::new(tool_name).with_arguments(arguments), and close removed clients outside the global map lock.
Expose Tauri commands returning Result<Value, String> or Result<(), String>:
#[tauri::command]
pub async fn mcp_stdio_start(
state: tauri::State<'_, McpStdioManager>,
config: McpStdioConfig,
) -> Result<Value, String>;
#[tauri::command]
pub async fn mcp_stdio_list_tools(
state: tauri::State<'_, McpStdioManager>,
server_name: String,
) -> Result<Value, String>;
#[tauri::command]
pub async fn mcp_stdio_call_tool(
state: tauri::State<'_, McpStdioManager>,
server_name: String,
tool_name: String,
arguments: Option<serde_json::Map<String, Value>>,
) -> Result<Value, String>;
#[tauri::command]
pub async fn mcp_stdio_stop(
state: tauri::State<'_, McpStdioManager>,
server_name: String,
) -> Result<(), String>;Register McpStdioManager::default() with .manage(...), export pub mod mcp, and add all four commands to generate_handler!.
- Step 5: Run Rust tests and check the Tauri crate
Run: cargo test --manifest-path app/src-tauri/Cargo.toml cmd::mcp::tests
Expected: PASS.
Run: cargo check --manifest-path app/src-tauri/Cargo.toml
Expected: PASS.
- Step 6: Commit the Rust client
git add app/src-tauri/Cargo.toml app/src-tauri/Cargo.lock app/src-tauri/src/cmd/mcp.rs app/src-tauri/src/cmd/mod.rs app/src-tauri/src/lib.rs
git commit -m "feat: run stdio MCP servers with Rust SDK"Task 4: Adapt Rust stdio tools into AI SDK tools
Files:
Create:
app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.tsCreate:
app/src/core/service/dataManageService/aiEngine/AIMCPStdio.tsStep 1: Write a failing stdio bridge test with a fake invoke function
import { describe, expect, it, vi } from "vitest";
import { createStdioMCPTools, startStdioMCPServer } from "./AIMCPStdio";
it("starts a normalized command and exposes approval-required tools", async () => {
const invoke = vi.fn(async (command: string) =>
command === "mcp_stdio_start"
? [{ name: "read", description: "Read", inputSchema: { type: "object", properties: {} } }]
: { content: [{ type: "text", text: "ok" }] },
);
const server: AIMCPServerConfig = {
name: "local",
transport: { type: "stdio", command: "node", args: ["server.js"] },
enabled: true,
enabledTools: ["read"],
cachedTools: [],
};
const descriptors = await startStdioMCPServer(server, invoke);
const tools = createStdioMCPTools(server, descriptors, invoke);
expect(invoke).toHaveBeenCalledWith("mcp_stdio_start", {
config: { serverName: "local", command: "node", args: ["server.js"], env: {} },
});
expect(tools.mcp__local__read.needsApproval).toBe(true);
await tools.mcp__local__read.execute({}, { toolCallId: "call-1", messages: [] });
expect(invoke).toHaveBeenLastCalledWith("mcp_stdio_call_tool", {
serverName: "local",
toolName: "read",
arguments: {},
});
});- Step 2: Run the test and verify it fails because the bridge does not exist
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.ts
Expected: FAIL with an import-resolution error.
- Step 3: Implement the typed Tauri bridge and dynamic tool adapter
import { invoke as tauriInvoke } from "@tauri-apps/api/core";
import { dynamicTool, jsonSchema, type ToolSet } from "ai";
export type AIMCPInvoke = <T>(command: string, args?: Record<string, unknown>) => Promise<T>;
export async function startStdioMCPServer(
server: AIMCPServerConfig,
invoke: AIMCPInvoke = tauriInvoke,
): Promise<AIMCPToolDescriptor[]>;
export function createStdioMCPTools(
server: AIMCPServerConfig,
descriptors: AIMCPToolDescriptor[],
invoke: AIMCPInvoke = tauriInvoke,
): ToolSet;Validate every Rust-returned descriptor before using it. Construct tools only for enabledTools, use jsonSchema(descriptor.inputSchema), namespace with createMCPToolName, set needsApproval: true, and call the focused Rust command in execute. Export stopStdioMCPServer for disable, delete, and changed-definition cleanup.
- Step 4: Run the stdio bridge test and verify it passes
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.ts
Expected: PASS.
- Step 5: Commit the stdio adapter
git add app/src/core/service/dataManageService/aiEngine/AIMCPStdio.ts app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.ts
git commit -m "feat: adapt stdio MCP tools for AI SDK"Task 5: Unify remote and local MCP preparation
Files:
Modify:
app/src/core/service/dataManageService/aiEngine/AIMCP.tsModify:
app/src/core/service/dataManageService/aiEngine/AIMCP.test.tsModify:
app/src/core/service/dataManageService/aiEngine/AIEngine.tsxStep 1: Replace old URL-only fixtures with transport-aware failing tests
const remote: AIMCPServerConfig = {
name: "docs",
transport: {
type: "streamable-http",
url: "https://example.com/mcp",
headers: { "X-Client": "project-graph" },
},
enabled: true,
enabledTools: ["search"],
cachedTools: [],
};
const local: AIMCPServerConfig = {
name: "local",
transport: { type: "stdio", command: "node", args: ["server.js"] },
enabled: true,
enabledTools: ["read"],
cachedTools: [
{
serverName: "local",
name: "read",
modelName: "mcp__local__read",
inputSchema: { type: "object", properties: {} },
},
],
};Assert that remote factories receive headers, stdio preparation does not create an HTTP client, stdio tools remain approval-required, and HTTP clients still close when later setup fails.
- Step 2: Run MCP tests and confirm old URL-only assumptions fail
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts
Expected: FAIL at old url configuration fields and missing stdio routing.
- Step 3: Route each transport to its official runtime
Update the remote factory mapping:
transport: {
type: "http",
url: config.transport.url,
headers: config.transport.headers,
redirect: "error",
fetch: tauriFetch as typeof globalThis.fetch,
}Update prepareMCPTools so HTTP servers create request-scoped AI SDK clients while stdio servers build tools from cached descriptors and the persistent Rust bridge. Update discoverMCPTools so HTTP connects temporarily and stdio calls startStdioMCPServer.
In AIEngine, load AIMCPStore.load(), materialize server configs, and pass them to prepareMCPTools. Keep mcpRuntime.close responsible only for request-scoped HTTP clients.
- Step 4: Run all MCP unit tests
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.ts app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts
Expected: PASS.
- Step 5: Commit unified runtime preparation
git add app/src/core/service/dataManageService/aiEngine/AIMCP.ts app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts app/src/core/service/dataManageService/aiEngine/AIEngine.tsx
git commit -m "feat: prepare remote and local MCP tools"Task 6: Replace the MCP form with JSON-first management
Files:
Modify:
app/src/sub/AIToolsWindow.tsxStep 1: Replace URL form state with snapshot and JSON editor state
const [snapshot, setSnapshot] = useState<AIMCPSnapshot>(createEmptyMCPState());
const [documentText, setDocumentText] = useState('{\n "mcpServers": {}\n}');
useEffect(() => {
void AIMCPStore.load()
.then((loaded) => {
setSnapshot(loaded);
setDocumentText(serializeMCPConfigDocument(loaded.definitions));
})
.catch((error) => toast.error(`่ฏปๅ MCP ้
็ฝฎๅคฑ่ดฅ๏ผ${getErrorMessage(error)}`));
}, []);Render the existing shadcn Textarea with font-mono, a visible local-secret warning, and a โๆ ก้ชๅนถไฟๅญโ button. On save, parse the complete document first, stop removed or changed stdio servers, then call saveDefinitions. A parse error leaves Store and cards unchanged.
- Step 2: Add local-process trust before stdio discovery
const fingerprint = createMCPDefinitionFingerprint(definition);
if (runtime.trustedDefinitionHash !== fingerprint) {
const approved = await Dialog.confirm(
"ๅ
่ฎธ่ฟ่กๆฌๅฐ MCP ๆๅกๅจ๏ผ",
formatStdioTrustDescription(definition.transport),
{ destructive: true },
);
if (!approved) return;
}The description includes the executable, quoted arguments, cwd, and environment variable names, but not environment values. After a successful handshake and discovery, persist the fingerprint and cached descriptors. A rejected or failed start does not enable the server.
- Step 3: Update server cards and runtime actions
Display URL for HTTP and command plus arguments for stdio. Keep tool schema folds and per-tool switches. Enabling requires cached tools; disabling or deleting stdio calls stopStdioMCPServer. Deleting also rewrites the canonical JSON editor text from the remaining definitions.
Every async action catches the error once at the UI boundary and displays it with toast or dialog; no console.error fallback is added.
- Step 4: Run TypeScript type checking
Run: pnpm --filter @graphif/project-graph type-check
Expected: PASS, or only pre-existing diagnostics recorded before this change.
- Step 5: Commit the management UI
git add app/src/sub/AIToolsWindow.tsx
git commit -m "feat: manage MCP servers with JSON configuration"Task 7: Verify the complete MCP feature
Files:
Test:
app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.tsTest:
app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.tsTest:
app/src/core/service/dataManageService/aiEngine/AIMCP.test.tsTest:
app/src-tauri/src/cmd/mcp.rsStep 1: Run focused frontend tests
Run: pnpm exec vitest run app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.ts app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts
Expected: PASS.
- Step 2: Run Rust MCP tests and crate checking
Run: cargo test --manifest-path app/src-tauri/Cargo.toml cmd::mcp::tests
Expected: PASS.
Run: cargo check --manifest-path app/src-tauri/Cargo.toml
Expected: PASS.
- Step 3: Run project-wide checks
Run: pnpm --filter @graphif/project-graph type-check
Expected: PASS or documented pre-existing diagnostics only.
Run: pnpm test --run
Expected: PASS or documented pre-existing failures only.
- Step 4: Review formatting, secrets, and final diff
Run: git diff --check
Expected: no whitespace errors.
Run: git status --short
Expected: only intentional MCP implementation files are modified. Confirm tests contain placeholder credentials only and no local MCP environment values or headers were committed.
- Step 5: Commit final corrections if verification changed files
git add app/src app/src-tauri docs/superpowers/plans/2026-07-14-mcp-json-stdio.md
git commit -m "fix: complete MCP JSON and stdio integration"