From f87d750bb1f8c69a6f61aec5cd6cbe7fab7190d4 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Tue, 3 Feb 2026 20:57:06 -0800 Subject: [PATCH] feat: add converting-mcps-to-skills bundled skill (#811) Co-authored-by: Letta --- .../converting-mcps-to-skills/SKILL.md | 171 +++++++ .../references/skill-templates.md | 211 +++++++++ .../scripts/mcp-http.ts | 429 ++++++++++++++++++ .../scripts/mcp-stdio.ts | 359 +++++++++++++++ .../scripts/package.json | 13 + 5 files changed, 1183 insertions(+) create mode 100644 src/skills/builtin/converting-mcps-to-skills/SKILL.md create mode 100644 src/skills/builtin/converting-mcps-to-skills/references/skill-templates.md create mode 100644 src/skills/builtin/converting-mcps-to-skills/scripts/mcp-http.ts create mode 100644 src/skills/builtin/converting-mcps-to-skills/scripts/mcp-stdio.ts create mode 100644 src/skills/builtin/converting-mcps-to-skills/scripts/package.json diff --git a/src/skills/builtin/converting-mcps-to-skills/SKILL.md b/src/skills/builtin/converting-mcps-to-skills/SKILL.md new file mode 100644 index 0000000..aa454e0 --- /dev/null +++ b/src/skills/builtin/converting-mcps-to-skills/SKILL.md @@ -0,0 +1,171 @@ +--- +name: converting-mcps-to-skills +description: Connect to MCP (Model Context Protocol) servers and create skills for repeated use. Load when a user wants to use an MCP server, connect to external tools via MCP, or when they mention MCP, model context protocol, or specific MCP servers. +--- + +# Converting MCP Servers to Skills + +Letta Code is not itself an MCP client, but as a general computer-use agent, you can easily connect to any MCP server using the scripts in this skill. + +## What is MCP? + +MCP (Model Context Protocol) is a standard for exposing tools to AI agents. MCP servers provide tools via JSON-RPC, either over: +- **HTTP** - Server running at a URL (e.g., `http://localhost:3001/mcp`) +- **stdio** - Server runs as a subprocess, communicating via stdin/stdout + +## Quick Start: Connecting to an MCP Server + +### Step 1: Determine the transport type + +Ask the user: +- Is it an HTTP server (has a URL)? +- Is it a stdio server (runs via command like `npx`, `node`, `python`)? + +### Step 2: Test the connection + +**For HTTP servers:** +```bash +npx tsx /scripts/mcp-http.ts list-tools + +# With auth header +npx tsx /scripts/mcp-http.ts --header "Authorization: Bearer KEY" list-tools +``` + +**For stdio servers:** +```bash +# First, install dependencies (one time) +cd /scripts && npm install + +# Then connect +npx tsx /scripts/mcp-stdio.ts "" list-tools + +# Examples +npx tsx /scripts/mcp-stdio.ts "npx -y @modelcontextprotocol/server-filesystem ." list-tools +npx tsx /scripts/mcp-stdio.ts "python server.py" list-tools +``` + +### Step 3: Explore available tools + +```bash +# List all tools +... list-tools + +# Get schema for a specific tool +... info + +# Test calling a tool +... call '{"arg": "value"}' +``` + +## Creating a Dedicated Skill + +When an MCP server will be used repeatedly, create a dedicated skill for it. This makes future use easier and documents the server's capabilities. + +### Decision: Simple vs Rich Skill + +**Simple skill** (just SKILL.md): +- Good for straightforward servers +- Documents how to use the parent skill's scripts with this specific server +- No additional scripts needed + +**Rich skill** (SKILL.md + scripts/): +- Good for frequently-used servers +- Includes convenience wrapper scripts with defaults baked in +- Provides a simpler interface than the generic scripts + +See `references/skill-templates.md` for templates. + +## Built-in Scripts Reference + +### mcp-http.ts - HTTP Transport + +Connects to MCP servers over HTTP. No dependencies required. + +```bash +npx tsx mcp-http.ts [options] [args] + +Commands: + list-tools List available tools + list-resources List available resources + info Show tool schema + call '' Call a tool + +Options: + --header "K: V" Add HTTP header (repeatable) + --timeout Request timeout (default: 30000) +``` + +**Examples:** +```bash +# Basic usage +npx tsx mcp-http.ts http://localhost:3001/mcp list-tools + +# With authentication +npx tsx mcp-http.ts http://localhost:3001/mcp --header "Authorization: Bearer KEY" list-tools + +# Call a tool +npx tsx mcp-http.ts http://localhost:3001/mcp call vault '{"action":"search","query":"notes"}' +``` + +### mcp-stdio.ts - stdio Transport + +Connects to MCP servers that run as subprocesses. Requires npm install first. + +```bash +# One-time setup +cd /scripts && npm install + +npx tsx mcp-stdio.ts "" [options] [args] + +Actions: + list-tools List available tools + list-resources List available resources + info Show tool schema + call '' Call a tool + +Options: + --env "KEY=VALUE" Set environment variable (repeatable) + --cwd Set working directory + --timeout Request timeout (default: 30000) +``` + +**Examples:** +```bash +# Filesystem server +npx tsx mcp-stdio.ts "npx -y @modelcontextprotocol/server-filesystem ." list-tools + +# With environment variable +npx tsx mcp-stdio.ts "node server.js" --env "API_KEY=xxx" list-tools + +# Call a tool +npx tsx mcp-stdio.ts "python server.py" call read_file '{"path":"./README.md"}' +``` + +## Common MCP Servers + +Here are some well-known MCP servers: + +| Server | Transport | Command/URL | +|--------|-----------|-------------| +| Filesystem | stdio | `npx -y @modelcontextprotocol/server-filesystem ` | +| GitHub | stdio | `npx -y @modelcontextprotocol/server-github` | +| Brave Search | stdio | `npx -y @modelcontextprotocol/server-brave-search` | +| obsidian-mcp-plugin | HTTP | `http://localhost:3001/mcp` | + +## Troubleshooting + +**"Cannot connect" error:** +- For HTTP: Check the URL is correct and server is running +- For stdio: Check the command works when run directly in terminal + +**"Authentication required" error:** +- Add `--header "Authorization: Bearer YOUR_KEY"` for HTTP +- Or `--env "API_KEY=xxx"` for stdio servers that need env vars + +**stdio "npm install" error:** +- Run `cd /scripts && npm install` first +- The stdio client requires the MCP SDK + +**Tool call fails:** +- Use `info ` to see the expected input schema +- Ensure JSON arguments match the schema diff --git a/src/skills/builtin/converting-mcps-to-skills/references/skill-templates.md b/src/skills/builtin/converting-mcps-to-skills/references/skill-templates.md new file mode 100644 index 0000000..a787dc8 --- /dev/null +++ b/src/skills/builtin/converting-mcps-to-skills/references/skill-templates.md @@ -0,0 +1,211 @@ +# Skill Templates for MCP Servers + +Use these templates when creating dedicated skills for MCP servers. + +## Naming Rules (from Agent Skills spec) + +The `name` field must: +- Be lowercase letters, numbers, and hyphens only (`a-z`, `0-9`, `-`) +- Be 1-64 characters +- Not start or end with a hyphen +- Not contain consecutive hyphens (`--`) +- Match the parent directory name exactly + +Examples: `using-obsidian-mcp`, `mcp-filesystem`, `github-mcp` + +## Simple Skill Template (Documentation Only) + +Use this when: +- The MCP server has straightforward tools +- Usage patterns are simple +- No convenience wrappers needed + +Keep SKILL.md under 500 lines. Move detailed docs to `references/`. + +```markdown +--- +name: using- +description: . Use when . +# Optional fields: +# license: MIT +# compatibility: Requires network access to +# metadata: +# author: +# version: "1.0" +--- + +# Using + + + +## Prerequisites + +- +- + +## Quick Start + +```bash +# Set up (if auth required) +export ="your-key" + +# List available tools +npx tsx ~/.letta/skills/converting-mcps-to-skills/scripts/mcp-.ts list-tools + +# Common operations +npx tsx ... call '{"action":"..."}' +``` + +## Available Tools + + + +### + + +```bash +call '{"param": "value"}' +``` + +## Environment Variables + +- `` - +``` + +--- + +## Rich Skill Template (With Convenience Scripts) + +Use this when: +- The MCP server will be used frequently +- You want simpler command-line interface +- Server has complex auth or configuration + +### Directory Structure + +``` +using-/ +├── SKILL.md +└── scripts/ + └── .ts # Convenience wrapper +``` + +### SKILL.md Template + +```markdown +--- +name: using- +description: . Use when . +# Optional: license, compatibility, metadata (see simple template) +--- + +# Using + + + +## Prerequisites + +- + +## Quick Start + +```bash +# Set API key (if needed) +export _API_KEY="your-key" + +# List tools +npx tsx /scripts/.ts list-tools + +# Call a tool +npx tsx /scripts/.ts '{"action":"..."}' +``` + +## Commands + + + +## Environment Variables + +- `_API_KEY` - API key for authentication +- `_URL` - Override server URL (default: ) +``` + +### Convenience Wrapper Template (scripts/.ts) + +```typescript +#!/usr/bin/env npx tsx +/** + * CLI - Convenience wrapper for MCP server + * + * Usage: + * npx tsx .ts list-tools + * npx tsx .ts '{"action":"..."}' + */ + +// Configuration +const DEFAULT_URL = ""; +const API_KEY = process.env._API_KEY; +const SERVER_URL = process.env._URL || DEFAULT_URL; + +// Import the parent skill's HTTP client +// For HTTP servers, you can inline the client code or import it +// For stdio, import from the parent skill + +// ... implementation similar to obsidian-mcp.ts ... +// Key differences: +// - Bake in the server URL/command as defaults +// - Simplify the CLI interface for this specific server +// - Add server-specific convenience commands if needed +``` + +--- + +## Example: Simple Skill for Filesystem Server + +```markdown +--- +name: using-mcp-filesystem +description: Access local filesystem via MCP filesystem server. Use when user wants to read, write, or search files via MCP. +--- + +# Using MCP Filesystem Server + +Access local files via the official MCP filesystem server. + +## Quick Start + +```bash +# Start by listing available tools +npx tsx ~/.letta/skills/converting-mcps-to-skills/scripts/mcp-stdio.ts \ + "npx -y @modelcontextprotocol/server-filesystem ." list-tools + +# Read a file +npx tsx ... call read_file '{"path":"./README.md"}' + +# List directory +npx tsx ... call list_directory '{"path":"."}' + +# Search files +npx tsx ... call search_files '{"path":".","pattern":"*.ts"}' +``` + +## Available Tools + +- `read_file` - Read file contents +- `write_file` - Write content to file +- `list_directory` - List directory contents +- `search_files` - Search for files by pattern +- `get_file_info` - Get file metadata +``` + +--- + +## Example: Rich Skill for Obsidian + +See `~/.letta/skills/using-obsidian-mcp-plugin/` for a complete example of a rich skill with a convenience wrapper script. + +Key features of the rich skill: +- Custom `obsidian-mcp.ts` script with defaults baked in +- Simplified CLI: just `call vault '{"action":"list"}'` +- Environment variables for auth: `OBSIDIAN_MCP_KEY` +- Comprehensive documentation of all tools diff --git a/src/skills/builtin/converting-mcps-to-skills/scripts/mcp-http.ts b/src/skills/builtin/converting-mcps-to-skills/scripts/mcp-http.ts new file mode 100644 index 0000000..466fad9 --- /dev/null +++ b/src/skills/builtin/converting-mcps-to-skills/scripts/mcp-http.ts @@ -0,0 +1,429 @@ +#!/usr/bin/env npx tsx +/** + * MCP HTTP Client - Connect to any MCP server over HTTP + * + * Usage: + * npx tsx mcp-http.ts [args] + * + * Commands: + * list-tools List available tools + * list-resources List available resources + * call '' Call a tool with JSON arguments + * + * Options: + * --header "Key: Value" Add HTTP header (can be repeated) + * + * Examples: + * npx tsx mcp-http.ts http://localhost:3001/mcp list-tools + * npx tsx mcp-http.ts http://localhost:3001/mcp call vault '{"action":"list"}' + * npx tsx mcp-http.ts http://localhost:3001/mcp --header "Authorization: Bearer KEY" list-tools + */ + +interface JsonRpcRequest { + jsonrpc: "2.0"; + method: string; + params?: object; + id: number; +} + +interface JsonRpcResponse { + jsonrpc: "2.0"; + result?: unknown; + error?: { + code: number; + message: string; + data?: unknown; + }; + id: number; +} + +interface ParsedArgs { + url: string; + command: string; + commandArgs: string[]; + headers: Record; +} + +function parseArgs(): ParsedArgs { + const args = process.argv.slice(2); + const headers: Record = {}; + let url = ""; + let command = ""; + const commandArgs: string[] = []; + + let i = 0; + while (i < args.length) { + const arg = args[i]; + if (!arg) { + i++; + continue; + } + + if (arg === "--header" || arg === "-H") { + const headerValue = args[++i]; + if (headerValue) { + const colonIndex = headerValue.indexOf(":"); + if (colonIndex > 0) { + const key = headerValue.slice(0, colonIndex).trim(); + const value = headerValue.slice(colonIndex + 1).trim(); + headers[key] = value; + } + } + } else if (arg === "--help" || arg === "-h") { + printUsage(); + process.exit(0); + } else if (!url && arg.startsWith("http")) { + url = arg; + } else if (!command) { + command = arg; + } else { + commandArgs.push(arg); + } + i++; + } + + return { url, command, commandArgs, headers }; +} + +// Session state +let sessionId: string | null = null; +let initialized = false; +let requestHeaders: Record = {}; +let serverUrl = ""; + +async function rawMcpRequest( + method: string, + params?: object, +): Promise<{ response: JsonRpcResponse; newSessionId?: string }> { + const request: JsonRpcRequest = { + jsonrpc: "2.0", + method, + params, + id: Date.now(), + }; + + const headers: Record = { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + ...requestHeaders, + }; + + if (sessionId) { + headers["Mcp-Session-Id"] = sessionId; + } + + try { + const fetchResponse = await fetch(serverUrl, { + method: "POST", + headers, + body: JSON.stringify(request), + }); + + // Capture session ID from response + const newSessionId = + fetchResponse.headers.get("Mcp-Session-Id") || undefined; + + if (!fetchResponse.ok) { + const text = await fetchResponse.text(); + if (fetchResponse.status === 401) { + throw new Error( + `Authentication required.\n` + + `Add --header "Authorization: Bearer YOUR_KEY" or similar.`, + ); + } + + // Try to parse as JSON-RPC error + try { + const errorResponse = JSON.parse(text) as JsonRpcResponse; + return { response: errorResponse, newSessionId }; + } catch { + throw new Error( + `HTTP ${fetchResponse.status}: ${fetchResponse.statusText}\n${text}`, + ); + } + } + + const contentType = fetchResponse.headers.get("content-type") || ""; + + // Handle JSON response + if (contentType.includes("application/json")) { + const jsonResponse = (await fetchResponse.json()) as JsonRpcResponse; + return { response: jsonResponse, newSessionId }; + } + + // Handle SSE stream (simplified - just collect all events) + if (contentType.includes("text/event-stream")) { + const text = await fetchResponse.text(); + const dataLines = text + .split("\n") + .filter((line) => line.startsWith("data: ")) + .map((line) => line.slice(6)); + + for (let i = dataLines.length - 1; i >= 0; i--) { + const line = dataLines[i]; + if (!line) continue; + try { + const parsed = JSON.parse(line); + if (parsed.jsonrpc === "2.0") { + return { response: parsed as JsonRpcResponse, newSessionId }; + } + } catch { + // Continue to previous line + } + } + throw new Error("No valid JSON-RPC response found in SSE stream"); + } + + throw new Error(`Unexpected content type: ${contentType}`); + } catch (error) { + if (error instanceof TypeError && error.message.includes("fetch")) { + throw new Error( + `Cannot connect to ${serverUrl}\nIs the MCP server running?`, + ); + } + throw error; + } +} + +async function ensureInitialized(): Promise { + if (initialized) return; + + const { response, newSessionId } = await rawMcpRequest("initialize", { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { + name: "mcp-http-cli", + version: "1.0.0", + }, + }); + + if (newSessionId) { + sessionId = newSessionId; + } + + if (response.error) { + throw new Error(`Initialization failed: ${response.error.message}`); + } + + // Send initialized notification + await rawMcpRequest("notifications/initialized", {}); + + initialized = true; +} + +async function mcpRequest( + method: string, + params?: object, +): Promise { + await ensureInitialized(); + + const { response, newSessionId } = await rawMcpRequest(method, params); + + if (newSessionId) { + sessionId = newSessionId; + } + + return response; +} + +async function listTools(): Promise { + const response = await mcpRequest("tools/list"); + + if (response.error) { + console.error("Error:", response.error.message); + process.exit(1); + } + + const result = response.result as { + tools: Array<{ name: string; description: string; inputSchema: object }>; + }; + + console.log("Available tools:\n"); + for (const tool of result.tools) { + console.log(` ${tool.name}`); + if (tool.description) { + console.log(` ${tool.description}\n`); + } else { + console.log(); + } + } + + console.log(`\nTotal: ${result.tools.length} tools`); + console.log("\nUse 'call ' to invoke a tool"); +} + +async function listResources(): Promise { + const response = await mcpRequest("resources/list"); + + if (response.error) { + console.error("Error:", response.error.message); + process.exit(1); + } + + const result = response.result as { + resources: Array<{ uri: string; name: string; description?: string }>; + }; + + if (!result.resources || result.resources.length === 0) { + console.log("No resources available."); + return; + } + + console.log("Available resources:\n"); + for (const resource of result.resources) { + console.log(` ${resource.uri}`); + console.log(` ${resource.name}`); + if (resource.description) { + console.log(` ${resource.description}`); + } + console.log(); + } +} + +async function callTool(toolName: string, argsJson: string): Promise { + let args: object; + try { + args = JSON.parse(argsJson || "{}"); + } catch { + console.error(`Invalid JSON: ${argsJson}`); + process.exit(1); + } + + const response = await mcpRequest("tools/call", { + name: toolName, + arguments: args, + }); + + if (response.error) { + console.error("Error:", response.error.message); + if (response.error.data) { + console.error("Details:", JSON.stringify(response.error.data, null, 2)); + } + process.exit(1); + } + + console.log(JSON.stringify(response.result, null, 2)); +} + +async function getToolSchema(toolName: string): Promise { + const response = await mcpRequest("tools/list"); + + if (response.error) { + console.error("Error:", response.error.message); + process.exit(1); + } + + const result = response.result as { + tools: Array<{ name: string; description: string; inputSchema: object }>; + }; + + const tool = result.tools.find((t) => t.name === toolName); + if (!tool) { + console.error(`Tool not found: ${toolName}`); + console.error( + `Available tools: ${result.tools.map((t) => t.name).join(", ")}`, + ); + process.exit(1); + } + + console.log(`Tool: ${tool.name}\n`); + if (tool.description) { + console.log(`Description: ${tool.description}\n`); + } + console.log("Input Schema:"); + console.log(JSON.stringify(tool.inputSchema, null, 2)); +} + +function printUsage(): void { + console.log(`MCP HTTP Client - Connect to any MCP server over HTTP + +Usage: npx tsx mcp-http.ts [options] [args] + +Commands: + list-tools List available tools with descriptions + list-resources List available resources + info Show tool schema/parameters + call '' Call a tool with JSON arguments + +Options: + --header, -H "K: V" Add HTTP header (repeatable) + --help, -h Show this help + +Examples: + # List tools from a server + npx tsx mcp-http.ts http://localhost:3001/mcp list-tools + + # With authentication + npx tsx mcp-http.ts http://localhost:3001/mcp --header "Authorization: Bearer KEY" list-tools + + # Get tool schema + npx tsx mcp-http.ts http://localhost:3001/mcp info vault + + # Call a tool + npx tsx mcp-http.ts http://localhost:3001/mcp call vault '{"action":"list"}' +`); +} + +async function main(): Promise { + const { url, command, commandArgs, headers } = parseArgs(); + + if (!url) { + console.error("Error: URL is required\n"); + printUsage(); + process.exit(1); + } + + if (!command) { + console.error("Error: Command is required\n"); + printUsage(); + process.exit(1); + } + + // Set globals + serverUrl = url; + requestHeaders = headers; + + try { + switch (command) { + case "list-tools": + await listTools(); + break; + + case "list-resources": + await listResources(); + break; + + case "info": { + const [toolName] = commandArgs; + if (!toolName) { + console.error("Error: Tool name required"); + console.error("Usage: info "); + process.exit(1); + } + await getToolSchema(toolName); + break; + } + + case "call": { + const [toolName, argsJson] = commandArgs; + if (!toolName) { + console.error("Error: Tool name required"); + console.error("Usage: call ''"); + process.exit(1); + } + await callTool(toolName, argsJson || "{}"); + break; + } + + default: + console.error(`Unknown command: ${command}\n`); + printUsage(); + process.exit(1); + } + } catch (error) { + console.error("Error:", error instanceof Error ? error.message : error); + process.exit(1); + } +} + +main(); diff --git a/src/skills/builtin/converting-mcps-to-skills/scripts/mcp-stdio.ts b/src/skills/builtin/converting-mcps-to-skills/scripts/mcp-stdio.ts new file mode 100644 index 0000000..e6ccf9d --- /dev/null +++ b/src/skills/builtin/converting-mcps-to-skills/scripts/mcp-stdio.ts @@ -0,0 +1,359 @@ +#!/usr/bin/env npx tsx +/** + * MCP stdio Client - Connect to any MCP server over stdio + * + * NOTE: Requires npm install in this directory first: + * cd && npm install + * + * Usage: + * npx tsx mcp-stdio.ts "" [args] + * + * Commands: + * list-tools List available tools + * list-resources List available resources + * info Show tool schema + * call '' Call a tool with JSON arguments + * + * Options: + * --env "KEY=VALUE" Set environment variable (can be repeated) + * --cwd Set working directory for server + * + * Examples: + * npx tsx mcp-stdio.ts "node server.js" list-tools + * npx tsx mcp-stdio.ts "npx -y @modelcontextprotocol/server-filesystem ." list-tools + * npx tsx mcp-stdio.ts "python server.py" call my_tool '{"arg":"value"}' + * npx tsx mcp-stdio.ts "node server.js" --env "API_KEY=xxx" list-tools + */ + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +interface ParsedArgs { + serverCommand: string; + action: string; + actionArgs: string[]; + env: Record; + cwd?: string; +} + +function parseArgs(): ParsedArgs { + const args = process.argv.slice(2); + const env: Record = {}; + let cwd: string | undefined; + let serverCommand = ""; + let action = ""; + const actionArgs: string[] = []; + + let i = 0; + while (i < args.length) { + const arg = args[i]; + if (!arg) { + i++; + continue; + } + + if (arg === "--env" || arg === "-e") { + const envValue = args[++i]; + if (envValue) { + const eqIndex = envValue.indexOf("="); + if (eqIndex > 0) { + const key = envValue.slice(0, eqIndex); + const value = envValue.slice(eqIndex + 1); + env[key] = value; + } + } + } else if (arg === "--cwd") { + cwd = args[++i]; + } else if (arg === "--help" || arg === "-h") { + printUsage(); + process.exit(0); + } else if (!serverCommand) { + serverCommand = arg; + } else if (!action) { + action = arg; + } else { + actionArgs.push(arg); + } + i++; + } + + return { serverCommand, action, actionArgs, env, cwd }; +} + +function parseCommand(commandStr: string): { command: string; args: string[] } { + // Simple parsing - split on spaces, respecting quotes + const parts: string[] = []; + let current = ""; + let inQuote = false; + let quoteChar = ""; + + for (const char of commandStr) { + if ((char === '"' || char === "'") && !inQuote) { + inQuote = true; + quoteChar = char; + } else if (char === quoteChar && inQuote) { + inQuote = false; + quoteChar = ""; + } else if (char === " " && !inQuote) { + if (current) { + parts.push(current); + current = ""; + } + } else { + current += char; + } + } + if (current) { + parts.push(current); + } + + return { + command: parts[0] || "", + args: parts.slice(1), + }; +} + +let client: Client | null = null; +let transport: StdioClientTransport | null = null; + +async function connect( + serverCommand: string, + env: Record, + cwd?: string, +): Promise { + const { command, args } = parseCommand(serverCommand); + + if (!command) { + throw new Error("No command specified"); + } + + // Merge with process.env + const mergedEnv: Record = {}; + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + mergedEnv[key] = value; + } + } + Object.assign(mergedEnv, env); + + transport = new StdioClientTransport({ + command, + args, + env: mergedEnv, + cwd, + stderr: "pipe", + }); + + // Forward stderr for debugging + if (transport.stderr) { + transport.stderr.on("data", (chunk: Buffer) => { + process.stderr.write(`[server] ${chunk.toString()}`); + }); + } + + client = new Client( + { + name: "mcp-stdio-cli", + version: "1.0.0", + }, + { + capabilities: {}, + }, + ); + + await client.connect(transport); + return client; +} + +async function cleanup(): Promise { + if (client) { + try { + await client.close(); + } catch { + // Ignore cleanup errors + } + } +} + +async function listTools(client: Client): Promise { + const result = await client.listTools(); + + console.log("Available tools:\n"); + for (const tool of result.tools) { + console.log(` ${tool.name}`); + if (tool.description) { + console.log(` ${tool.description}\n`); + } else { + console.log(); + } + } + + console.log(`\nTotal: ${result.tools.length} tools`); + console.log("\nUse 'call ' to invoke a tool"); +} + +async function listResources(client: Client): Promise { + const result = await client.listResources(); + + if (!result.resources || result.resources.length === 0) { + console.log("No resources available."); + return; + } + + console.log("Available resources:\n"); + for (const resource of result.resources) { + console.log(` ${resource.uri}`); + console.log(` ${resource.name}`); + if (resource.description) { + console.log(` ${resource.description}`); + } + console.log(); + } +} + +async function getToolSchema(client: Client, toolName: string): Promise { + const result = await client.listTools(); + + const tool = result.tools.find((t) => t.name === toolName); + if (!tool) { + console.error(`Tool not found: ${toolName}`); + console.error( + `Available tools: ${result.tools.map((t) => t.name).join(", ")}`, + ); + process.exit(1); + } + + console.log(`Tool: ${tool.name}\n`); + if (tool.description) { + console.log(`Description: ${tool.description}\n`); + } + console.log("Input Schema:"); + console.log(JSON.stringify(tool.inputSchema, null, 2)); +} + +async function callTool( + client: Client, + toolName: string, + argsJson: string, +): Promise { + let args: Record; + try { + args = JSON.parse(argsJson || "{}"); + } catch { + console.error(`Invalid JSON: ${argsJson}`); + process.exit(1); + } + + const result = await client.callTool({ + name: toolName, + arguments: args, + }); + + console.log(JSON.stringify(result, null, 2)); +} + +function printUsage(): void { + console.log(`MCP stdio Client - Connect to any MCP server over stdio + +NOTE: Requires npm install in this directory first: + cd && npm install + +Usage: npx tsx mcp-stdio.ts "" [options] [args] + +Actions: + list-tools List available tools with descriptions + list-resources List available resources + info Show tool schema/parameters + call '' Call a tool with JSON arguments + +Options: + --env, -e "KEY=VALUE" Set environment variable (repeatable) + --cwd Set working directory for server + --help, -h Show this help + +Examples: + # List tools from filesystem server + npx tsx mcp-stdio.ts "npx -y @modelcontextprotocol/server-filesystem ." list-tools + + # With environment variable + npx tsx mcp-stdio.ts "node server.js" --env "API_KEY=xxx" list-tools + + # Call a tool + npx tsx mcp-stdio.ts "python server.py" call read_file '{"path":"./README.md"}' +`); +} + +async function main(): Promise { + const { serverCommand, action, actionArgs, env, cwd } = parseArgs(); + + if (!serverCommand) { + console.error("Error: Server command is required\n"); + printUsage(); + process.exit(1); + } + + if (!action) { + console.error("Error: Action is required\n"); + printUsage(); + process.exit(1); + } + + // Handle process exit + process.on("SIGINT", async () => { + await cleanup(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + await cleanup(); + process.exit(0); + }); + + try { + const connectedClient = await connect(serverCommand, env, cwd); + + switch (action) { + case "list-tools": + await listTools(connectedClient); + break; + + case "list-resources": + await listResources(connectedClient); + break; + + case "info": { + const [toolName] = actionArgs; + if (!toolName) { + console.error("Error: Tool name required"); + console.error("Usage: info "); + process.exit(1); + } + await getToolSchema(connectedClient, toolName); + break; + } + + case "call": { + const [toolName, argsJson] = actionArgs; + if (!toolName) { + console.error("Error: Tool name required"); + console.error("Usage: call ''"); + process.exit(1); + } + await callTool(connectedClient, toolName, argsJson || "{}"); + break; + } + + default: + console.error(`Unknown action: ${action}\n`); + printUsage(); + process.exit(1); + } + } catch (error) { + console.error("Error:", error instanceof Error ? error.message : error); + process.exit(1); + } finally { + await cleanup(); + } +} + +main(); diff --git a/src/skills/builtin/converting-mcps-to-skills/scripts/package.json b/src/skills/builtin/converting-mcps-to-skills/scripts/package.json new file mode 100644 index 0000000..98fbb0a --- /dev/null +++ b/src/skills/builtin/converting-mcps-to-skills/scripts/package.json @@ -0,0 +1,13 @@ +{ + "name": "mcp-client-scripts", + "version": "1.0.0", + "type": "module", + "description": "MCP client scripts for converting-mcps-to-skills", + "scripts": { + "http": "npx tsx mcp-http.ts", + "stdio": "npx tsx mcp-stdio.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.0" + } +}