feat: add converting-mcps-to-skills bundled skill (#811)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-02-03 20:57:06 -08:00
committed by GitHub
parent 16f680c50d
commit f87d750bb1
5 changed files with 1183 additions and 0 deletions

View File

@@ -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 <skill-path>/scripts/mcp-http.ts <url> list-tools
# With auth header
npx tsx <skill-path>/scripts/mcp-http.ts <url> --header "Authorization: Bearer KEY" list-tools
```
**For stdio servers:**
```bash
# First, install dependencies (one time)
cd <skill-path>/scripts && npm install
# Then connect
npx tsx <skill-path>/scripts/mcp-stdio.ts "<command>" list-tools
# Examples
npx tsx <skill-path>/scripts/mcp-stdio.ts "npx -y @modelcontextprotocol/server-filesystem ." list-tools
npx tsx <skill-path>/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 <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.
```bash
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:**
```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 <skill-path>/scripts && npm install
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:**
```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 <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
**stdio "npm install" error:**
- Run `cd <skill-path>/scripts && npm install` first
- The stdio client requires the MCP SDK
**Tool call fails:**
- Use `info <tool>` to see the expected input schema
- Ensure JSON arguments match the schema

View File

@@ -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-<server-name>
description: <What the server does>. Use when <trigger conditions>.
# Optional fields:
# license: MIT
# compatibility: Requires network access to <service>
# metadata:
# author: <author>
# version: "1.0"
---
# Using <Server Name>
<Brief description of what this MCP server provides.>
## Prerequisites
- <Server requirements, e.g., "Server running at http://localhost:3001/mcp">
- <Auth requirements if any>
## Quick Start
```bash
# Set up (if auth required)
export <ENV_VAR>="your-key"
# List available tools
npx tsx ~/.letta/skills/converting-mcps-to-skills/scripts/mcp-<transport>.ts <url-or-command> list-tools
# Common operations
npx tsx ... call <tool> '{"action":"..."}'
```
## Available Tools
<List the tools with brief descriptions and example calls>
### <tool-name>
<Description>
```bash
call <tool> '{"param": "value"}'
```
## Environment Variables
- `<VAR_NAME>` - <Description>
```
---
## 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-<server-name>/
├── SKILL.md
└── scripts/
└── <server>.ts # Convenience wrapper
```
### SKILL.md Template
```markdown
---
name: using-<server-name>
description: <What the server does>. Use when <trigger conditions>.
# Optional: license, compatibility, metadata (see simple template)
---
# Using <Server Name>
<Brief description>
## Prerequisites
- <Requirements>
## Quick Start
```bash
# 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 <tool> '{"action":"..."}'
```
## Commands
<Document the convenience wrapper commands>
## Environment Variables
- `<SERVER>_API_KEY` - API key for authentication
- `<SERVER>_URL` - Override server URL (default: <default-url>)
```
### Convenience Wrapper Template (scripts/<server>.ts)
```typescript
#!/usr/bin/env npx tsx
/**
* <Server Name> CLI - Convenience wrapper for <server> MCP server
*
* Usage:
* npx tsx <server>.ts list-tools
* npx tsx <server>.ts <tool> '{"action":"..."}'
*/
// Configuration
const DEFAULT_URL = "<default-server-url>";
const API_KEY = process.env.<SERVER>_API_KEY;
const SERVER_URL = process.env.<SERVER>_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

View File

@@ -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 <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();

View File

@@ -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 <this-directory> && npm install
*
* 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 { 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<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 client: Client | null = null;
let transport: StdioClientTransport | null = null;
async function connect(
serverCommand: string,
env: Record<string, string>,
cwd?: string,
): Promise<Client> {
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);
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<void> {
if (client) {
try {
await client.close();
} catch {
// Ignore cleanup errors
}
}
}
async function listTools(client: Client): Promise<void> {
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 <tool> <json-args>' to invoke a tool");
}
async function listResources(client: Client): Promise<void> {
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<void> {
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<void> {
let args: Record<string, unknown>;
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 <this-directory> && npm install
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);
}
// 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 <tool>");
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 <tool> '<json-args>'");
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();

View File

@@ -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"
}
}