Install
$ agentstack add skill-sergeyitaly-claude-skill-deployer-mcp-server-creation Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
MCP Server Creation — claude-skills-deployer pattern
This skill captures the end-to-end pattern for a stdio MCP server bundled inside a VS Code extension, as implemented in extension/resources/mcp-servers/cli/index.js and wired by extension/src/mcpCli.ts + extension/src/extension.ts.
1. File layout
extension/
resources/
mcp-servers/
/
index.js # bundled server — source of truth
src/
mcp.ts # deploy + register helpers (TypeScript)
extension.ts # auto-start + status bar wiring
2. Server skeleton (Node.js stdio, no SDK dependency)
#!/usr/bin/env node
"use strict";
const { spawn, execFileSync } = require("node:child_process");
const crypto = require("node:crypto");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const readline = require("node:readline");
// --- JSON-RPC helpers ---
function send(msg) { process.stdout.write(JSON.stringify(msg) + "\n"); }
function respond(id, result) { send({ jsonrpc: "2.0", id, result }); }
function respondError(id, c, m) { send({ jsonrpc: "2.0", id, error: { code: c, message: m } }); }
// --- CRITICAL: track pending async tool calls before exiting ---
let _pendingOps = 0;
let _stdinEnded = false;
function _tryExit() {
if (_stdinEnded && _pendingOps === 0) process.exit(0);
}
// --- Dispatch ---
async function dispatchTool(id, toolName, args) {
try {
let result;
switch (toolName) {
case "my_tool": {
const output = await doSomethingAsync(args);
// CRITICAL: wrap in content array — raw objects produce "no output" in Claude
result = { content: [{ type: "text", text: output }] };
break;
}
default:
respondError(id, -32601, `Tool not found: ${toolName}`);
return;
}
respond(id, result);
} catch (e) {
// Errors must also use content format, with isError: true
respond(id, { content: [{ type: "text", text: e.message }], isError: true });
}
}
// --- Readline loop ---
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
rl.on("line", async (line) => {
if (!line.trim()) return;
let msg;
try { msg = JSON.parse(line); } catch { return; }
const { id, method, params } = msg;
if (!method) return;
switch (method) {
case "initialize":
respond(id, {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "my-server", version: "1.0" },
});
break;
case "notifications/initialized": break;
case "tools/list":
respond(id, { tools: [{ name: "my_tool", description: "...", inputSchema: { type: "object", properties: {}, required: [] } }] });
break;
case "tools/call":
// CRITICAL: increment before await, decrement in finally
_pendingOps++;
try { await dispatchTool(id, params?.name, params?.arguments || {}); }
finally { _pendingOps--; _tryExit(); }
break;
case "ping":
if (id != null) respond(id, {});
break;
default:
if (id != null) respondError(id, -32601, `Method not supported: ${method}`);
}
});
// CRITICAL: don't call process.exit(0) directly here — check pending ops first
process.stdin.on("end", () => { _stdinEnded = true; _tryExit(); });
3. Two bugs that always appear — fix them first
Bug 1 — Premature exit kills async tool calls
Symptom: Only synchronous tools (list_available_clis uses execFileSync) respond. Async tools (run_command uses spawn) return nothing because process.exit(0) fires the moment stdin closes (e.g. file-based test input), before child processes finish.
Wrong:
process.stdin.on("end", () => process.exit(0));
Correct (pending-ops gate):
let _pendingOps = 0;
let _stdinEnded = false;
function _tryExit() {
if (_stdinEnded && _pendingOps === 0) process.exit(0);
}
// In tools/call handler:
_pendingOps++;
try { await dispatchTool(id, params?.name, params?.arguments || {}); }
finally { _pendingOps--; _tryExit(); }
// At end of file:
process.stdin.on("end", () => { _stdinEnded = true; _tryExit(); });
> Note: this bug does NOT manifest when the server runs as a real MCP server (Claude > Code keeps stdin open). It only surfaces in file-based test scripts.
Bug 2 — Raw result objects produce "no output" in Claude
Symptom: Tool calls complete with (mcp____ completed with no output). The server is responding, but Claude's MCP layer discards responses that don't match the MCP tools/call content format.
Wrong:
respond(id, { stdout: "git version 2.47.0", exitCode: 0 });
respond(id, { clis: [...] });
Correct:
respond(id, {
content: [{ type: "text", text: "stdout:\ngit version 2.47.0\nexitCode: 0" }],
isError: false,
});
// Errors:
respond(id, {
content: [{ type: "text", text: e.message }],
isError: true,
});
The tools/call MCP spec (2024-11-05) requires:
{ "content": [{ "type": "text", "text": "..." }], "isError": false }
4. Allow-list security pattern (CLI server example)
const DEFAULT_ALLOWED = ["az","aws","git","kubectl","helm","terraform","gcloud","docker","gh","dotnet","node","npm"];
function getAllowedClis() {
// Optionally read from config file; normalise .cmd/.exe for Windows matching
return DEFAULT_ALLOWED.map(c => c.toLowerCase().replace(/\.(cmd|exe)$/, ""));
}
// In run_command handler:
const cliNorm = rawCli.toLowerCase().replace(/\.(cmd|exe)$/, "");
if (!getAllowedClis().includes(cliNorm)) {
throw new Error(`"${rawCli}" is not in the allow-list. Allowed: ${getAllowedClis().join(", ")}`);
}
// On Windows use shell:true so .cmd/.bat wrappers launch correctly
const useShell = process.platform === "win32";
const proc = spawn(cli, args, { env, cwd, shell: useShell, windowsHide: true });
5. Deploy helper (TypeScript — mcpCli.ts pattern)
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
const SERVER_DIR = path.join(os.homedir(), ".claude", "mcp-servers", "my-server");
const SERVER_PATH = path.join(SERVER_DIR, "index.js");
const SERVER_KEY = "my-server"; // key in mcpServers JSON
const AGENT_CONFIGS: Record = {
claude: path.join(os.homedir(), ".claude.json"),
cursor: path.join(os.homedir(), ".cursor", "mcp.json"),
kiro: path.join(os.homedir(), ".kiro", "settings", "mcp.json"),
};
export function needsSetup(): boolean {
return !fs.existsSync(SERVER_PATH)
|| !JSON.parse(fs.readFileSync(AGENT_CONFIGS.claude, "utf-8") || "{}")
?.mcpServers?.[SERVER_KEY];
}
export function getServerStatus(): { enabled: boolean; activeAgents: string[] } {
const activeAgents = Object.entries(AGENT_CONFIGS)
.filter(([, p]) => {
try { return Boolean(JSON.parse(fs.readFileSync(p,"utf-8"))?.mcpServers?.[SERVER_KEY]); }
catch { return false; }
})
.map(([id]) => id);
return { enabled: activeAgents.length > 0, activeAgents };
}
export async function enableServer(extensionPath: string, workspaceDirs: string[], log: (s:string)=>void): Promise {
// 1. Copy bundled server
const src = path.join(extensionPath, "resources", "mcp-servers", "my-server", "index.js");
fs.mkdirSync(SERVER_DIR, { recursive: true });
fs.copyFileSync(src, SERVER_PATH);
fs.chmodSync(SERVER_PATH, 0o755);
// 2. Write config (allow-list, timeout, workspace log path)
const config = { allowedClis: [...], timeout: 300000, workspaceLogPath: workspaceDirs[0] ? path.join(workspaceDirs[0], ".claude", "mcp-usage.jsonl") : undefined };
fs.writeFileSync(path.join(SERVER_DIR, "config.json"), JSON.stringify(config, null, 2));
// 3. Register in each agent config
for (const [agentId, configPath] of Object.entries(AGENT_CONFIGS)) {
try {
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8") || "{}");
cfg.mcpServers = cfg.mcpServers ?? {};
cfg.mcpServers[SERVER_KEY] = { command: "node", args: [SERVER_PATH, "--config", path.join(SERVER_DIR, "config.json")] };
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
log(`Registered ${SERVER_KEY} for ${agentId}.`);
} catch (err) {
log(`Could not configure ${agentId}: ${(err as Error).message}`);
}
}
}
6. Extension auto-start on activation (extension.ts pattern)
Wire inside the existing 5 s setTimeout block that also handles the filesystem server:
// extension.ts — inside the setTimeout(() => { ... }, 5000) block
if (needsCliMcpSetup()) {
void enableOfficialCliServer(context.extensionPath, workspaceDirs, log).then(() => {
log("CLI MCP server: auto-started.");
refreshCliMcpStatusBar();
});
} else {
log("CLI MCP server: already configured.");
refreshCliMcpStatusBar();
}
7. Status bar item
// Module-level variable
let mcpCliStatusBarItem: vscode.StatusBarItem;
// In activate():
mcpCliStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 91.5);
context.subscriptions.push(mcpCliStatusBarItem);
// Refresh function (module-level so it can be called from multiple places):
function refreshCliMcpStatusBar() {
const status = getCliMcpServerStatus();
if (status.enabled) {
const agents = status.activeAgents.join(", ");
mcpCliStatusBarItem.text = `$(terminal-cmd) CLI MCP · ${agents}`;
mcpCliStatusBarItem.tooltip = `CLI MCP server active for: ${agents}.\nClick to disable.`;
mcpCliStatusBarItem.command = "claudeSkills.disableCliMcpServer";
mcpCliStatusBarItem.backgroundColor = undefined;
} else {
mcpCliStatusBarItem.text = `$(warning) CLI MCP: setup needed`;
mcpCliStatusBarItem.tooltip = `CLI MCP server not configured. Click to enable.`;
mcpCliStatusBarItem.command = "claudeSkills.enableCliMcpServer";
mcpCliStatusBarItem.backgroundColor = new vscode.ThemeColor("statusBarItem.warningBackground");
}
mcpCliStatusBarItem.show();
}
// Call after: enable command, disable command, auto-start, and in refreshAllImpl
8. Health dialog integration
In claudeSkills.showMcpHealth command handler, add a section after the filesystem block:
const cliStatus = getCliMcpServerStatus();
lines.push(``, `── CLI MCP Server ──`);
if (cliStatus.enabled) {
lines.push(
`Status: Connected ✓`,
`Agents: ${cliStatus.activeAgents.join(", ")}`,
`CLIs: az, aws, git, kubectl, helm, terraform, gcloud, docker, gh, dotnet, node, npm`,
);
} else {
lines.push(
`Status: Setup needed ✗`,
`Action: Run "Enable CLI MCP Server" from the command palette`,
);
}
9. Testing without a running session (PowerShell)
The MCP server speaks JSON-RPC over stdio. Test it directly without Claude:
$serverPath = "C:\Users\...\extension\resources\mcp-servers\cli\index.js"
$messages = @(
'{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}',
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}',
'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_available_clis","arguments":{}}}',
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"run_command","arguments":{"cli":"git","args":["--version"]}}}'
) -join "`n"
$tmpIn = [System.IO.Path]::GetTempFileName()
$tmpOut = [System.IO.Path]::GetTempFileName()
$tmpErr = [System.IO.Path]::GetTempFileName()
[System.IO.File]::WriteAllText($tmpIn, $messages + "`n", [System.Text.Encoding]::UTF8)
Start-Process "node" "`"$serverPath`"" `
-RedirectStandardInput $tmpIn -RedirectStandardOutput $tmpOut -RedirectStandardError $tmpErr `
-NoNewWindow -PassThru -Wait | Out-Null
Get-Content $tmpOut -Raw
Remove-Item $tmpIn,$tmpOut,$tmpErr -ErrorAction SilentlyContinue
Why temp files instead of pipe + ReadToEnd(): Redirecting stdout AND stderr simultaneously with ReadToEnd() deadlocks on Windows when the server spawns child processes (stdout buffer fills while you're blocked on stderr). Always use separate temp files.
Required message order: initialize → notifications/initialized → tool calls. The server ignores messages before initialize and errors on unknown methods.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: sergeyitaly
- Source: sergeyitaly/claude-skill-deployer
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.