project-graph Documentation

A node-based visual tool for organizing thoughts and notes in a non-linear way.

RAW Doc

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:

json
{
  "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, optional args, cwd, and env.
  • streamable-http: url and optional string headers.

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:

ts
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:

  1. 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.
  2. 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.json

  • Modify: pnpm-lock.yaml

  • Create: app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts

  • Create: app/src/core/service/dataManageService/aiEngine/AIMCP.ts

  • Step 1: Add failing tests for server-name normalization, tool namespacing, filtering, and approval decoration

ts
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 AIMCP does 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/mcp 1.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, and needsApproval: true decoration
ts
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.ts

  • Modify: app/src/core/service/dataManageService/aiEngine/AIEngine.tsx

  • Test: app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts

  • Step 1: Add a failing lifecycle test that closes every successfully created client when a later connection fails

ts
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 with client.tools(), and return an idempotent close() callback

ts
export type PreparedMCPTools = {
  tools: ToolSet;
  descriptors: AIMCPToolDescriptor[];
  close(): Promise<void>;
};
  • Step 4: Merge MCP tools into AIEngine and close clients on setup failure, stream completion, and abort

  • Step 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.ts

  • Create: app/src/core/service/dataManageService/aiEngine/AIToolUIPart.ts

  • Modify: app/src/sub/AIWindow.tsx

  • Step 1: Add failing tests for static and dynamic tool-part detection and display names

ts
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 useChat to expose addToolApprovalResponse, configure lastAssistantMessageIsCompleteWithApprovalResponses, and pass the approval callback into message rendering

  • Step 5: Render approval-requested with existing shadcn Button components and call addToolApprovalResponse({ 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.ts

  • Create: app/src/core/service/dataManageService/aiEngine/AISkills.ts

  • Step 1: Add failing tests for SKILL.md parsing, project-over-user precedence, invalid frontmatter, resource traversal, and activation deduplication

ts
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 AISkills does not exist

  • Step 3: Implement YAML frontmatter parsing with the existing yaml dependency and deterministic catalog merging

  • Step 4: Implement user/project .agents/skills/*/SKILL.md discovery through Tauri FS, with project skills disabled until trusted

  • Step 5: Implement activate_skill and read_skill_resource AI SDK tools with enum-constrained names, 256 KiB UTF-8 limits, and root containment checks

  • Step 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.ts

  • Modify: app/src/core/service/dataManageService/aiEngine/AIEngine.tsx

  • Create: app/src/core/service/dataManageService/aiEngine/AISkillSession.test.ts

  • Create: app/src/core/service/dataManageService/aiEngine/AISkillSession.ts

  • Step 1: Add a failing test that builds system context from the current catalog and activated snapshots without duplicate bodies

ts
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 activatedSkills snapshots and add get/activate methods that preserve existing messages and memory

  • Step 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.tsx

  • Step 1: Use installed shadcn Button, Input, Switch, Collapsible, and Badge components to list, add, test, enable, and delete unauthenticated HTTP MCP servers

  • Step 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.ts

  • Test: app/src/core/service/dataManageService/aiEngine/AIToolUIPart.test.ts

  • Test: app/src/core/service/dataManageService/aiEngine/AISkills.test.ts

  • Test: app/src/core/service/dataManageService/aiEngine/AISkillSession.test.ts

  • Step 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 --check and 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.toml and app/src-tauri/Cargo.lock: add rmcp and 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.ts

  • Create: app/src/core/service/dataManageService/aiEngine/AIMCPConfig.ts

  • Step 1: Write failing parser and serializer tests

text
/* 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
ts
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
bash
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.ts

  • Modify: app/src/core/service/dataManageService/aiEngine/AIMCPConfig.test.ts

  • Modify: app/src/core/service/dataManageService/aiEngine/AIMCP.ts

  • Step 1: Add failing reconciliation tests

ts
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
ts
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
bash
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.toml

  • Modify: app/src-tauri/Cargo.lock

  • Create: app/src-tauri/src/cmd/mcp.rs

  • Modify: app/src-tauri/src/cmd/mod.rs

  • Modify: app/src-tauri/src/lib.rs

  • Step 1: Add rmcp and Tokio process support

Run from app/src-tauri:

bash
cargo add [email protected] --no-default-features --features client,transport-child-process

Change the direct Tokio dependency to:

toml
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "process", "sync"] }
  • Step 2: Write failing Rust tests for validation and missing clients
rust
#[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 rmcp manager
rust
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>:

rust
#[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
bash
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.ts

  • Create: app/src/core/service/dataManageService/aiEngine/AIMCPStdio.ts

  • Step 1: Write a failing stdio bridge test with a fake invoke function

ts
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
ts
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
bash
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.ts

  • Modify: app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts

  • Modify: app/src/core/service/dataManageService/aiEngine/AIEngine.tsx

  • Step 1: Replace old URL-only fixtures with transport-aware failing tests

ts
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:

ts
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
bash
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.tsx

  • Step 1: Replace URL form state with snapshot and JSON editor state

ts
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
ts
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
bash
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.ts

  • Test: app/src/core/service/dataManageService/aiEngine/AIMCPStdio.test.ts

  • Test: app/src/core/service/dataManageService/aiEngine/AIMCP.test.ts

  • Test: app/src-tauri/src/cmd/mcp.rs

  • Step 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
bash
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"