loading…
Search for a command to run...
loading…
A meta MCP server that orchestrates other MCP servers by lazily connecting to them and exposing their tools as JavaScript libraries. It allows users to execute
A meta MCP server that orchestrates other MCP servers by lazily connecting to them and exposing their tools as JavaScript libraries. It allows users to execute JavaScript code that programmatically interacts with multiple MCP servers within a unified environment.
jsmcp exists for cases where an agent needs to do more than a single MCP tool call.
Most MCP clients are great at one tool call at a time, but awkward when work requires:
jsmcp solves that by exposing approved MCP tools as JavaScript namespaces. Instead of forcing the model to juggle many separate tool invocations, it can discover what is available and then write a small amount of JavaScript to use those tools programmatically.
In practice, this means:
jsmcp constrains access to whatever servers and tools you allow in a presetConfig is read from $XDG_CONFIG_HOME/jsmcp/ or, if XDG_CONFIG_HOME is not set, ~/.config/jsmcp/. Exactly one of config.json, config.yaml, or config.yml must exist there.
Use jsmcp when you want agents to treat MCP tools more like a small programmable API surface than a sequence of isolated button presses.
This is especially useful when an agent needs to:
npm install -g @alesya_h/jsmcp
Or run it without installing globally:
npx @alesya_h/jsmcp run
jsmcp run
jsmcp run work
jsmcp server work --port 3000
jsmcp client --profile work --port 3000
jsmcp auth
jsmcp auth firefox_devtools
If you are running from a source checkout instead of an installed package, replace jsmcp with node src/index.js, for example node src/index.js run.
run starts the meta-MCP server directly over stdio.
server starts a long-lived daemon on ws://127.0.0.1:<port>/mcp, loading the chosen preset once and keeping the underlying MCP server connections warm.
client exposes a stdio MCP server that proxies raw MCP/JSON-RPC messages to server over WebSocket. It accepts --port <number> to choose which daemon to connect to, and can optionally pass --profile <name> to require that the daemon is running the expected preset.
run, server, and client all accept an optional preset as either a positional argument or --profile <name>. The default daemon port is 41528.
Use jsmcp auth to manage OAuth for remote servers. With no arguments it lists remote servers that have OAuth enabled. With a server name it starts the OAuth flow for that server.
If no graphical environment is detected, or if you pass --no-browser, jsmcp auth <server> prints the authorization URL and waits for either the localhost callback or a pasted callback URL/code.
This repo includes systemd/jsmcp.service, a user unit that starts jsmcp server from the globally installed CLI.
Install it with:
npm install -g .
mkdir -p ~/.config/systemd/user
ln -sfn "$PWD/systemd/jsmcp.service" ~/.config/systemd/user/jsmcp.service
systemctl --user daemon-reload
systemctl --user enable --now jsmcp.service
Useful commands:
systemctl --user status jsmcp.service
journalctl --user -u jsmcp.service -f
systemctl --user restart jsmcp.service
The checked-in unit starts the default preset on the default daemon port and resolves jsmcp through the user's actual login shell from getent passwd.
The config file may be JSON or YAML and uses these top-level keys:
servers: server definitionspresets: optional overrides for which servers and tools are exposed to the agentServer names must be valid JavaScript identifiers because execute_code() exposes them directly as globals.
jsmcp accepts both OpenCode MCP config style and the overlapping Claude Code MCP style for the common fields:
type: "local" or type: "stdio"type: "remote", type: "http", or type: "sse"command: ["cmd", "arg1"] or command: "cmd" with args: ["arg1"]environment or envSupported servers.<name> fields:
type: required; one of local, stdio, remote, http, ssedescription: optional string shown in list_servers()enabled: optional boolean; defaults to truetimeout: optional number in milliseconds used for initial tool discoveryFor local / stdio servers:
command: required; non-empty string or non-empty arrayargs: optional array; appended to command when command is a string, and also accepted when command is an arrayenv: optional object of environment variablesenvironment: optional object of environment variables; merged with env, and wins on duplicate keyscwd: optional working directoryFor remote / HTTP / SSE servers:
url: required stringheaders: optional object of request headersoauth: optional OAuth configSupported oauth forms:
null, or true: enable OAuth with default behaviorfalse: disable OAuth for that serverclientIdclientSecretscopeSupported value substitutions in string fields:
{env:NAME}: expand from the current environment${NAME}: Claude Code-style environment expansion${NAME:-default}: Claude Code-style expansion with fallback{file:path}: replace with file contentsFor {file:path}:
~/... resolves from the user home directoryIf presets is omitted, the default preset includes every server with enabled !== false and allows all of that server's tools.
If presets is present, it is an object of preset names. Each preset is an object of per-server overrides layered on top of the server definitions:
presets.default: optional overrides for the default presetpresets.work: additional named preset overridesWithin a preset, server rules work like this:
true: include that server and allow all its toolsfalse: exclude that server from that preset"tool_name": include only that exact tool{ "regex": "..." } selectors{ "glob": "..." } selectorsIf a server has enabled: false in servers, adding it to a preset enables it for that preset.
Example:
{
"servers": {
"math": {
"type": "stdio",
"description": "Basic arithmetic tools",
"command": "node",
"args": ["/absolute/path/to/math-server.js"],
"env": {
"LOG_LEVEL": "debug"
},
"cwd": "${PWD}"
},
"docs": {
"type": "http",
"description": "Documentation search and retrieval",
"url": "https://example.com/mcp",
"headers": {
"Authorization": "Bearer ${DOCS_TOKEN}"
},
"oauth": {
"scope": "docs.read"
}
}
},
"presets": {
"default": {
"math": ["add", { "glob": "mul_*" }],
"docs": [{ "regex": "(search|fetch)" }]
},
"work": {
"docs": true
}
}
}
Compatibility notes:
env, type: "stdio", type: "http", type: "sse", and command plus args are supportedtype: "local", type: "remote", command arrays, and environment are also supportedheadersHelper and advanced OAuth fields like callbackPort or authServerMetadataUrl are not supported yetOAuth tokens and registration state are stored in $XDG_DATA_HOME/jsmcp/oauth.json or ~/.local/share/jsmcp/oauth.json.
list_serverslist_toolsexecute_codefetch_logsclear_logsjsmcp startslist_servers() is the required first step so the agent can learn what capabilities are availablelist_tools(server) before using a server in execute_code() so you know the exact tool names, aliases, and schemaslist_tools(server) returns only the tools allowed for that server in the presetexecute_code({ code, data?, timeoutMs? }) does not manage server lifecycle; it can only use servers that are already startedexecute_code({ code, ... }) whenever the work would require more than a single tool callconsole.log, console.info, console.warn, and console.error inside execute_code() are stored for fetch_logs()fetch_logs() drains the log buffer on readexecute_codeexecute_code runs JavaScript as the body of an async function.
Started servers are injected as globals. Each allowed MCP tool becomes a function on that server object. Prefer underscore aliases when available.
If you pass data, it is exposed to the script as the global variable data. This is useful for strings or structured values that would otherwise need escaping inside the code string.
You should call list_tools(server) before using a server in execute_code(). For multi-step work, prefer writing JavaScript instead of trying to mentally chain several tool calls.
Example:
return await math.add({ a: 2, b: 5 });
With data:
return data.message;
If the MCP tool returns structuredContent, that is what the JavaScript call resolves to. So the example above can return:
{
"sum": 7
}
If a tool name is not a valid JavaScript identifier, prefer its underscore alias:
return await math.tool_name({ value: 1 });
The original tool name still works with bracket access:
return await math["tool-name"]({ value: 1 });
Добавь это в claude_desktop_config.json и перезапусти Claude Desktop.
{
"mcpServers": {
"programmatic-mcp": {
"command": "npx",
"args": []
}
}
}PRs, issues, code search, CI status
Database, auth and storage
Reference / test server with prompts, resources, and tools.
Secure file operations with configurable access controls.