index.ts•15 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { spawn } from 'child_process';
class CurlMCPServer {
private server: Server;
private readonly MAX_OUTPUT_SIZE = 1024 * 1024; // 1MB
constructor() {
this.server = new Server({
name: 'curl-mcp-server',
version: '1.0.0',
capabilities: {
tools: {},
},
});
this.setupToolHandlers();
this.setupErrorHandling();
}
private setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'curl_get',
description: 'Execute a GET request using curl',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The URL to request',
},
headers: {
type: 'array',
items: { type: 'string' },
description: 'Optional headers in format "Key: Value"',
},
timeout: {
type: 'number',
description: 'Timeout in seconds (max 30)',
maximum: 30,
default: 10,
},
follow_redirects: {
type: 'boolean',
description: 'Follow HTTP redirects',
default: true,
},
},
required: ['url'],
},
},
{
name: 'curl_post',
description: 'Execute a POST request using curl',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The URL to request',
},
data: {
type: 'string',
description: 'POST data to send',
},
content_type: {
type: 'string',
description: 'Content-Type header',
default: 'application/json',
},
headers: {
type: 'array',
items: { type: 'string' },
description: 'Optional headers in format "Key: Value"',
},
timeout: {
type: 'number',
description: 'Timeout in seconds (max 30)',
maximum: 30,
default: 10,
},
},
required: ['url', 'data'],
},
},
{
name: 'curl_custom',
description: 'Execute a custom curl command with full control',
inputSchema: {
type: 'object',
properties: {
args: {
type: 'array',
items: { type: 'string' },
description: 'Curl arguments (without the curl command itself)',
},
timeout: {
type: 'number',
description: 'Timeout in seconds (max 30)',
maximum: 30,
default: 10,
},
},
required: ['args'],
},
},
{
name: 'curl_download',
description: 'Download a file using curl and return metadata',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The URL to download from',
},
output_file: {
type: 'string',
description: 'Output file path (optional, uses URL filename if not provided)',
},
timeout: {
type: 'number',
description: 'Timeout in seconds (max 30)',
maximum: 30,
default: 30,
},
},
required: ['url'],
},
},
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'curl_get':
return await this.handleGetRequest(args);
case 'curl_post':
return await this.handlePostRequest(args);
case 'curl_custom':
return await this.handleCustomRequest(args);
case 'curl_download':
return await this.handleDownload(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
private async handleGetRequest(args: any) {
const { url, headers = [], timeout = 10, follow_redirects = true } = args;
this.validateUrl(url);
const curlArgs = [
'-s', // Silent mode
'-i', // Include headers in output
'--max-time', timeout.toString(),
];
if (follow_redirects) {
curlArgs.push('-L');
}
// Add headers
headers.forEach((header: string) => {
curlArgs.push('-H', header);
});
curlArgs.push(url);
const result = await this.executeCurl(curlArgs, timeout * 1000);
return this.formatResponse(result, 'GET', url);
}
private async handlePostRequest(args: any) {
const { url, data, content_type = 'application/json', headers = [], timeout = 10 } = args;
this.validateUrl(url);
const curlArgs = [
'-s', // Silent mode
'-i', // Include headers in output
'-X', 'POST',
'--max-time', timeout.toString(),
'-H', `Content-Type: ${content_type}`,
'-d', data,
];
// Add additional headers
headers.forEach((header: string) => {
curlArgs.push('-H', header);
});
curlArgs.push(url);
const result = await this.executeCurl(curlArgs, timeout * 1000);
return this.formatResponse(result, 'POST', url);
}
private async handleCustomRequest(args: any) {
const { args: curlArgs, timeout = 10 } = args;
// Basic security check - prevent dangerous operations
this.validateCurlArgs(curlArgs);
const finalArgs = ['-s', '-i', '--max-time', timeout.toString(), ...curlArgs];
const result = await this.executeCurl(finalArgs, timeout * 1000);
return this.formatResponse(result, 'CUSTOM', curlArgs.join(' '));
}
private async handleDownload(args: any) {
const { url, output_file, timeout = 30 } = args;
this.validateUrl(url);
const curlArgs = [
'-s', // Silent mode
'-I', // Head request to get metadata
'--max-time', timeout.toString(),
url,
];
// First, get file metadata
const metadataResult = await this.executeCurl(curlArgs, timeout * 1000);
// Then download if output_file is specified
let downloadResult = null;
if (output_file) {
const downloadArgs = [
'-s',
'--max-time', timeout.toString(),
'-o', output_file,
url,
];
downloadResult = await this.executeCurl(downloadArgs, timeout * 1000);
}
return {
content: [
{
type: 'text',
text: `Download operation completed for: ${url}\n\n` +
`Metadata:\n${metadataResult.stdout}\n\n` +
(downloadResult ? `Download saved to: ${output_file}\n` : 'Metadata only (no file downloaded)\n') +
(metadataResult.stderr ? `Errors: ${metadataResult.stderr}` : ''),
},
],
};
}
private async executeCurl(args: string[], timeoutMs: number): Promise<{ stdout: string, stderr: string, exitCode: number }> {
return new Promise((resolve, reject) => {
const child = spawn('curl', args, {
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
let outputSize = 0;
const timeout = setTimeout(() => {
child.kill('SIGTERM');
reject(new McpError(ErrorCode.InternalError, 'Curl command timed out'));
}, timeoutMs);
child.stdout.on('data', (data) => {
outputSize += data.length;
if (outputSize > this.MAX_OUTPUT_SIZE) {
child.kill('SIGTERM');
reject(new McpError(ErrorCode.InternalError, 'Output size exceeded maximum limit'));
return;
}
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
clearTimeout(timeout);
resolve({
stdout,
stderr,
exitCode: code || 0,
});
});
child.on('error', (error) => {
clearTimeout(timeout);
reject(new McpError(ErrorCode.InternalError, `Failed to execute curl: ${error.message}`));
});
});
}
private validateUrl(url: string) {
try {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new McpError(ErrorCode.InvalidParams, 'Only HTTP and HTTPS URLs are allowed');
}
} catch (error) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid URL format');
}
}
private validateCurlArgs(args: string[]) {
const dangerousFlags = [
'--config', '-K', // Config file
'--output', '-o', // Output to file (controlled separately)
'--remote-name', '-O', // Save with remote name
'--upload-file', '-T', // Upload file
'--form', '-F', // Form upload
'--data-binary', // Binary data
'--trace', '--trace-ascii', // Trace to file
];
for (const arg of args) {
for (const dangerous of dangerousFlags) {
if (arg.startsWith(dangerous)) {
throw new McpError(
ErrorCode.InvalidParams,
`Dangerous curl flag not allowed: ${dangerous}`
);
}
}
}
}
private formatResponse(result: { stdout: string, stderr: string, exitCode: number }, method: string, url: string) {
const { stdout, stderr, exitCode } = result;
let responseText = `Curl ${method} Request: ${url}\n`;
responseText += `Exit Code: ${exitCode}\n\n`;
if (stdout) {
// Try to separate headers and body
const parts = stdout.split('\r\n\r\n');
if (parts.length >= 2) {
responseText += `Headers:\n${parts[0]}\n\n`;
responseText += `Body:\n${parts.slice(1).join('\r\n\r\n')}`;
} else {
responseText += `Response:\n${stdout}`;
}
}
if (stderr) {
responseText += `\n\nErrors/Warnings:\n${stderr}`;
}
return {
content: [
{
type: 'text',
text: responseText,
},
],
};
}
private setupErrorHandling() {
this.server.onerror = (error) => {
console.error('[MCP Error]', error);
};
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Curl MCP Server running on stdio');
}
}
// Start the server
const server = new CurlMCPServer();
server.run().catch(console.error);