feat: add converting-mcps-to-skills bundled skill (#811)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
171
src/skills/builtin/converting-mcps-to-skills/SKILL.md
Normal file
171
src/skills/builtin/converting-mcps-to-skills/SKILL.md
Normal 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
|
||||
@@ -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
|
||||
429
src/skills/builtin/converting-mcps-to-skills/scripts/mcp-http.ts
Normal file
429
src/skills/builtin/converting-mcps-to-skills/scripts/mcp-http.ts
Normal 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();
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user