
Converting Mcps To Skills
- 14 installs
- 2.9k repo stars
- Updated August 3, 2026
- letta-ai/letta-code
Helps with ai & agent building tasks.
About
converting-mcps-to-skills is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- converting-mcps-to-skills
- AI & Agent Building
- AI-coding skill
Converting Mcps To Skills by the numbers
- 14 all-time installs (skills.sh)
- Ranked #11,240 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/letta-ai/letta-code --skill converting-mcps-to-skillsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14 |
|---|---|
| repo stars | ★ 2.9k |
| Last updated | August 3, 2026 |
| Repository | letta-ai/letta-code ↗ |
What it does
Helps with ai & agent building tasks.
Files
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:
npx tsx <SKILL_DIR>/scripts/mcp-http.ts <url> list-tools
# With auth header
npx tsx <SKILL_DIR>/scripts/mcp-http.ts <url> --header "Authorization: Bearer KEY" list-toolsWhere <SKILL_DIR> is the Skill Directory shown when the skill was loaded (visible in the injection header).
For stdio servers:
npx tsx <SKILL_DIR>/scripts/mcp-stdio.ts "<command>" list-tools
# Examples
npx tsx <SKILL_DIR>/scripts/mcp-stdio.ts "npx -y @modelcontextprotocol/server-filesystem ." list-tools
npx tsx <SKILL_DIR>/scripts/mcp-stdio.ts "python server.py" list-toolsStep 3: Explore available tools
# List all tools
... list-tools
# Get schema for a specific tool
... info <tool-name>
# Test calling a tool
... call <tool-name> '{"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.
npx tsx mcp-http.ts <url> [options] <command> [args]
Commands:
list-tools List available tools
list-resources List available resources
info <tool> Show tool schema
call <tool> '<json>' Call a tool
Options:
--header "K: V" Add HTTP header (repeatable)
--timeout <ms> Request timeout (default: 30000)Examples:
# 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. No dependencies required.
npx tsx mcp-stdio.ts "<command>" [options] <action> [args]
Actions:
list-tools List available tools
list-resources List available resources
info <tool> Show tool schema
call <tool> '<json>' Call a tool
Options:
--env "KEY=VALUE" Set environment variable (repeatable)
--cwd <path> Set working directory
--timeout <ms> Request timeout (default: 30000)Examples:
# 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 <path> |
| 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
Tool call fails:
- Use
info <tool>to see the expected input schema - Ensure JSON arguments match the schema
Skill Templates for MCP Servers
When to create a dedicated skill:
- One-off use: No skill needed - just use
converting-mcps-to-skillsscripts directly - Repeated use: Create a self-contained skill with customized scripts
Skills should be self-contained per the Agent Skills spec.
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-github-mcp, mcp-filesystem, slack-mcp
Skill Template
Use this template when creating a self-contained skill for an MCP server.
Directory Structure
using-<server-name>/
├── SKILL.md
└── scripts/
└── <server>.ts # Customized client (copied from converting-mcps-to-skills)SKILL.md Template
---
name: using-<server-name>
description: <What the server does>. Use when <trigger conditions>.
# Optional fields:
# license: MIT
# compatibility: Requires network access to <service>
---
# Using <Server Name>
<Brief description>
## Prerequisites
- <Requirements>
## Quick Start
Set API key (if needed)
export <SERVER>_API_KEY="your-key"
List tools
npx tsx <skill-path>/scripts/<server>.ts list-tools
Call a tool
npx tsx <skill-path>/scripts/<server>.ts call <tool> '{"arg":"value"}'
## Available Tools
<Document tools with examples>
## Environment Variables
- `<SERVER>_API_KEY` - API key for authentication
- `<SERVER>_URL` - Override server URL (default: <default-url>)Script Template (scripts/<server>.ts)
Copy the HTTP client from converting-mcps-to-skills/scripts/mcp-http.ts (or mcp-stdio.ts for stdio servers) and customize:
1. Set DEFAULT_URL to this server's URL 2. Rename the API key env var (e.g., GITHUB_MCP_KEY instead of generic) 3. Optionally simplify the CLI for common operations
The copied code is self-contained - no external dependencies for HTTP transport.
#!/usr/bin/env npx tsx
/**
* <Server Name> CLI - Self-contained MCP client
*/
// Customize these for your server
const DEFAULT_URL = "<server-url>";
const API_KEY = process.env.<SERVER>_API_KEY;
// Copy the rest of mcp-http.ts here and adjust as needed
// ...Example: Self-Contained Filesystem Skill
A complete example of a self-contained skill for the MCP filesystem server:
using-mcp-filesystem/
├── SKILL.md
└── scripts/
└── filesystem.ts # Copied and customized from mcp-stdio.tsSKILL.md:
---
name: using-mcp-filesystem
description: Access local filesystem via MCP. Use when user wants to read, write, or search files via MCP protocol.
---
# Using MCP Filesystem Server
Access local files via the official MCP filesystem server.
## Quick Start
npx tsx <skill-path>/scripts/filesystem.ts list-tools npx tsx <skill-path>/scripts/filesystem.ts call read_file '{"path":"./README.md"}'
## 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 metadatascripts/filesystem.ts: Copy converting-mcps-to-skills/scripts/mcp-stdio.ts and set the default command to:
const DEFAULT_COMMAND = "npx -y @modelcontextprotocol/server-filesystem .";#!/usr/bin/env npx tsx
/**
* MCP HTTP Client - Connect to any MCP server over HTTP
*
* Usage:
* npx tsx mcp-http.ts <url> <command> [args]
*
* Commands:
* list-tools List available tools
* list-resources List available resources
* call <tool> '<json>' 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<string, string>;
}
function parseArgs(): ParsedArgs {
const args = process.argv.slice(2);
const headers: Record<string, string> = {};
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<string, string> = {};
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<string, string> = {
"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<void> {
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<JsonRpcResponse> {
await ensureInitialized();
const { response, newSessionId } = await rawMcpRequest(method, params);
if (newSessionId) {
sessionId = newSessionId;
}
return response;
}
async function listTools(): Promise<void> {
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 <tool> <json-args>' to invoke a tool");
}
async function listResources(): Promise<void> {
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<void> {
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<void> {
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 <url> [options] <command> [args]
Commands:
list-tools List available tools with descriptions
list-resources List available resources
info <tool> Show tool schema/parameters
call <tool> '<json>' 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<void> {
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 <tool>");
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 <tool> '<json-args>'");
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();
#!/usr/bin/env npx tsx
/**
* MCP stdio Client - Connect to any MCP server over stdio
*
* Usage:
* npx tsx mcp-stdio.ts "<command>" <action> [args]
*
* Commands:
* list-tools List available tools
* list-resources List available resources
* info <tool> Show tool schema
* call <tool> '<json>' Call a tool with JSON arguments
*
* Options:
* --env "KEY=VALUE" Set environment variable (can be repeated)
* --cwd <path> 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 { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
interface JsonRpcRequest {
jsonrpc: "2.0";
method: string;
params?: object;
id: number;
}
interface JsonRpcNotification {
jsonrpc: "2.0";
method: string;
params?: object;
}
interface JsonRpcResponse {
jsonrpc: "2.0";
result?: unknown;
error?: {
code: number;
message: string;
data?: unknown;
};
id: number;
}
interface ParsedArgs {
serverCommand: string;
action: string;
actionArgs: string[];
env: Record<string, string>;
cwd?: string;
}
function parseArgs(): ParsedArgs {
const args = process.argv.slice(2);
const env: Record<string, string> = {};
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 serverProcess: ChildProcessWithoutNullStreams | null = null;
let stdoutBuffer = "";
let nextRequestId = 1;
const pendingRequests = new Map<
number,
{
resolve: (response: JsonRpcResponse) => void;
reject: (error: Error) => void;
}
>();
function handleStdout(chunk: Buffer): void {
stdoutBuffer += chunk.toString("utf8");
while (true) {
const newlineIndex = stdoutBuffer.indexOf("\n");
if (newlineIndex === -1) {
return;
}
const line = stdoutBuffer.slice(0, newlineIndex).trim();
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
if (!line) {
continue;
}
let message: unknown;
try {
message = JSON.parse(line);
} catch {
process.stderr.write(`[server stdout] ${line}\n`);
continue;
}
if (
typeof message !== "object" ||
message === null ||
!("id" in message) ||
typeof message.id !== "number"
) {
continue;
}
const pending = pendingRequests.get(message.id);
if (!pending) {
continue;
}
pendingRequests.delete(message.id);
pending.resolve(message as JsonRpcResponse);
}
}
function rejectPendingRequests(error: Error): void {
for (const pending of pendingRequests.values()) {
pending.reject(error);
}
pendingRequests.clear();
}
async function connect(
serverCommand: string,
env: Record<string, string>,
cwd?: string,
): Promise<void> {
const { command, args } = parseCommand(serverCommand);
if (!command) {
throw new Error("No command specified");
}
// Merge with process.env.
const mergedEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) {
mergedEnv[key] = value;
}
}
Object.assign(mergedEnv, env);
serverProcess = spawn(command, args, {
cwd,
env: mergedEnv,
stdio: ["pipe", "pipe", "pipe"],
});
serverProcess.stdout.on("data", handleStdout);
serverProcess.stderr.on("data", (chunk: Buffer) => {
process.stderr.write(`[server] ${chunk.toString()}`);
});
serverProcess.on("error", (error) => {
rejectPendingRequests(error);
});
serverProcess.on("exit", (code, signal) => {
rejectPendingRequests(
new Error(`MCP server exited with code ${code} signal ${signal}`),
);
});
const initializeResponse = await sendRequest("initialize", {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "mcp-stdio-cli",
version: "1.0.0",
},
});
if (initializeResponse.error) {
throw new Error(
`Initialization failed: ${initializeResponse.error.message}`,
);
}
sendNotification("notifications/initialized", {});
}
function sendMessage(message: JsonRpcRequest | JsonRpcNotification): void {
if (!serverProcess) {
throw new Error("MCP server is not connected");
}
serverProcess.stdin.write(`${JSON.stringify(message)}\n`);
}
function sendNotification(method: string, params?: object): void {
sendMessage({ jsonrpc: "2.0", method, params });
}
function sendRequest(
method: string,
params?: object,
): Promise<JsonRpcResponse> {
const id = nextRequestId++;
const request: JsonRpcRequest = { jsonrpc: "2.0", method, params, id };
return new Promise((resolve, reject) => {
pendingRequests.set(id, { resolve, reject });
sendMessage(request);
});
}
async function cleanup(): Promise<void> {
if (serverProcess && !serverProcess.killed) {
serverProcess.kill();
}
serverProcess = null;
}
async function listTools(): Promise<void> {
const response = await sendRequest("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 <tool> <json-args>' to invoke a tool");
}
async function listResources(): Promise<void> {
const response = await sendRequest("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 getToolSchema(toolName: string): Promise<void> {
const response = await sendRequest("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));
}
async function callTool(toolName: string, argsJson: string): Promise<void> {
let args: Record<string, unknown>;
try {
args = JSON.parse(argsJson || "{}");
} catch {
console.error(`Invalid JSON: ${argsJson}`);
process.exit(1);
}
const response = await sendRequest("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));
}
function printUsage(): void {
console.log(`MCP stdio Client - Connect to any MCP server over stdio
Usage: npx tsx mcp-stdio.ts "<command>" [options] <action> [args]
Actions:
list-tools List available tools with descriptions
list-resources List available resources
info <tool> Show tool schema/parameters
call <tool> '<json>' Call a tool with JSON arguments
Options:
--env, -e "KEY=VALUE" Set environment variable (repeatable)
--cwd <path> 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<void> {
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);
}
process.on("SIGINT", async () => {
await cleanup();
process.exit(0);
});
process.on("SIGTERM", async () => {
await cleanup();
process.exit(0);
});
try {
await connect(serverCommand, env, cwd);
switch (action) {
case "list-tools":
await listTools();
break;
case "list-resources":
await listResources();
break;
case "info": {
const [toolName] = actionArgs;
if (!toolName) {
console.error("Error: Tool name required");
console.error("Usage: info <tool>");
process.exit(1);
}
await getToolSchema(toolName);
break;
}
case "call": {
const [toolName, argsJson] = actionArgs;
if (!toolName) {
console.error("Error: Tool name required");
console.error("Usage: call <tool> '<json-args>'");
process.exit(1);
}
await callTool(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();