programmatic-mcp
Provides an execution environment for JavaScript code, allowing users to programmatically orchestrate and interact with multiple MCP servers and their tools through a unified interface.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@programmatic-mcpRun a script to add 123 and 456 using the math server tools."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
jsmcp
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:
several related tool calls
branching logic based on earlier results
loops, retries, or result aggregation
transforming tool output before the next call
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:
the agent first learns what servers and tools are available, while
jsmcpconstrains access to whatever servers and tools you allow in a presetthe agent can then write JavaScript for multi-step work
logs stay separate from return values so the code stays easier to reason about
Config 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.
Why
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:
combine results from several MCP tools
script workflows across one or more MCP servers
make decisions in code instead of repeatedly re-planning between tool calls
keep tool access constrained to a reviewed preset
Related MCP server: MCPMan
Install
npm install -g @alesya_h/jsmcpOr run it without installing globally:
npx @alesya_h/jsmcp runRun
jsmcp run
jsmcp run work
jsmcp server work --port 3000 --bind 0.0.0.0
jsmcp client --profile work --host 127.0.0.1 --port 3000
jsmcp client --profile work --port 3000 --session-id my-agent-session
jsmcp client --profile work --host remote.example.com --api-key "$JSMCP_API_KEY"
jsmcp status --profile work --host 127.0.0.1 --port 3000
jsmcp status kagi --tools --profile work --port 3000
jsmcp restart --host 127.0.0.1 --port 3000
jsmcp auth
jsmcp auth firefox_devtoolsIf 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://<bind>:<port>/mcp, starts every globally enabled MCP server once, and keeps those underlying connections warm. The chosen preset becomes the default profile for connections that do not request one. It binds to 0.0.0.0 by default and accepts --bind <host> to choose another bind address.
client exposes a stdio MCP server that proxies raw MCP/JSON-RPC messages to server over WebSocket. It accepts --host <host> and --port <number> to choose which daemon to connect to, can optionally pass --profile <name> to select that daemon-side profile, accepts --session-id <id> to reuse the same daemon-side log session across client reconnects, and accepts --api-key <key> to authenticate with a daemon whose API key differs from the local key file.
status connects to the daemon over HTTP, retrying until it is reachable, and prints the configured servers in the selected profile with their startup status or startup errors. Pass a server name to show only that server, and pass --tools to include each healthy server's allowed tools and descriptions. It accepts --host <host>, --port <number>, and --profile <name>.
restart asks a running daemon over HTTP to exit with a restart-request status. Run it under a supervisor such as the included systemd user service so the daemon is started again automatically. It accepts --host <host> and --port <number>.
run, server, and client all accept an optional preset as either a positional argument or --profile <name>. The default daemon port is 41528. If client --session-id is omitted, the client generates a random session id and reuses it for reconnects during that client process.
On first server start, jsmcp creates an API key at $XDG_CONFIG_HOME/jsmcp/api-key.txt, or ~/.config/jsmcp/api-key.txt if XDG_CONFIG_HOME is not set. Daemon WebSocket and HTTP API requests must include it in the X-JSMCP-API-Key header; unauthenticated requests receive 401. CLI commands that connect to the daemon read the key from JSMCP_API_KEY when it is set, otherwise from the local key file. jsmcp client also accepts --api-key <key>, which takes precedence over JSMCP_API_KEY.
The daemon also exposes the five meta tools through one JSON HTTP endpoint, plus a restart endpoint:
POST /api/call?tool=list_servers&profile=<name>
POST /api/call?tool=list_tools&profile=<name>
POST /api/call?tool=execute_code&sessionId=<id>&profile=<name>
POST /api/call?tool=fetch_logs&sessionId=<id>
POST /api/call?tool=clear_logs&sessionId=<id>
POST /api/restartThe /api/call request body is a JSON object matching the selected MCP tool arguments. HTTP callers may include sessionId in the query string to use a stable daemon-side log session. They may include profile to select which profile filters the server and tool view for that request.
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.
Pi Extension
The Pi extension lives in extensions/pi/. To load it directly from this checkout, run from the repository root:
mkdir -p ~/.pi/agent/extensions
ln -s "$PWD/extensions/pi" ~/.pi/agent/extensions/jsmcpMove any existing standalone copy aside before creating the link. Start jsmcp server separately, then run /reload in Pi. The extension exposes the five meta tools with a jsmcp_ prefix, including jsmcp_execute_code with rawText, timing, and session memory support. See its README for configuration and tests.
systemd User Service
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.serviceUseful commands:
systemctl --user status jsmcp.service
journalctl --user -u jsmcp.service -f
systemctl --user restart jsmcp.serviceThe 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.
Config
The config file may be JSON or YAML and uses these top-level keys:
servers: server definitionsjsmcp: optional jsmcp-specific settingspresets: optional overrides for which servers and tools are exposed to the agent
Server 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:
local servers:
type: "local"ortype: "stdio"remote servers:
type: "remote",type: "http", ortype: "sse"commands: either
command: ["cmd", "arg1"]orcommand: "cmd"withargs: ["arg1"]environment variables: either
environmentorenv
Supported servers.<name> fields:
type: required; one oflocal,stdio,remote,http,ssedescription: optional string shown inlist_servers()enabled: optional boolean; defaults totruetimeout: optional number in milliseconds used for initial tool discoverystrip_tool_prefix: optional string,true, orfalse; strings are removed from exposed tool names,trueinfers a shared prefix, andfalsedisables prefix stripping for that servernormalize_tool_names: optional boolean; converts exposed tool names tosnake_caseafter prefix strippingblocked_tools: optional server-level deny list; a tool name string or array of exact tool names,{ glob: "..." }, and{ regex: "..." }selectors. Selectors match final exposed tool names after prefix stripping and normalization, and blocked tools cannot be re-enabled by presets.
Supported jsmcp fields:
auto_strip_tool_prefixes: optional boolean; defaultfalse; iftrue, servers infer and strip shared tool-name prefixes unless overridden byservers.<name>.strip_tool_prefixnormalize_tool_names: optional boolean; defaultfalse; iftrue, servers expose tool names assnake_caseunless overridden byservers.<name>.normalize_tool_names
For local / stdio servers:
command: required; non-empty string or non-empty arrayargs: optional array; appended tocommandwhencommandis a string, and also accepted whencommandis an arrayenv: optional object of environment variablesenvironment: optional object of environment variables; merged withenv, and wins on duplicate keyscwd: optional working directory
For remote / HTTP / SSE servers:
url: required stringheaders: optional object of request headersoauth: optional OAuth config
Supported oauth forms:
omitted,
null, ortrue: enable OAuth with default behaviorfalse: disable OAuth for that serverobject with any of:
clientIdclientSecretscope
Supported 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 contents
For {file:path}:
relative paths are resolved relative to the config file directory
~/...resolves from the user home directoryabsolute paths are used as-is
If 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 presetany other preset name, such as
presets.work: additional named preset overrides
If a server strips prefixes or normalizes names, preset tool selectors match the final exposed tool names that agents see.
Server-level blocked_tools selectors are applied before preset allowlists. Use them for globally unsafe tools, and use presets for profile-specific whitelists.
Within a preset, server rules work like this:
omitted server rule: use the server definition as-is
true: include that server and allow all its toolsfalse: exclude that server from that preset"tool_name": include only that exact toolarray entries may be:
exact tool name strings
{ "regex": "..." }selectors{ "glob": "..." }selectors
If a server has enabled: false in servers, it is globally disabled and is not started or exposed by any 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:
Claude Code-style
env,type: "stdio",type: "http",type: "sse", andcommandplusargsare supportedOpenCode-style
type: "local",type: "remote", command arrays, andenvironmentare also supportedClaude Code-specific features such as
headersHelperand advanced OAuth fields likecallbackPortorauthServerMetadataUrlare not supported yet
OAuth tokens and registration state are stored in $XDG_DATA_HOME/jsmcp/oauth.json or ~/.local/share/jsmcp/oauth.json.
Exposed Tools
list_serverslist_toolsexecute_codefetch_logsclear_logs
Behavior
every server with
enabled !== falseis started once whenjsmcpstartslist_servers()is the required first step so the agent can learn what capabilities are availableyou must call
list_tools(server)before using a server inexecute_code()so you know the exact tool names, aliases, and schemaslist_servers()andlist_tools(server)return only the servers and tools allowed in the connection's selected profileexecute_code({ code, data?, timeoutMs?, rawText? })does not manage server lifecycle; it can only use servers that are already startedprefer
execute_code({ code, ... })whenever the work would require more than a single tool callexecute_code()exposes session-scoped in-memory JSON storage atjsmcp.memoryconsole.log,console.info,console.warn, andconsole.errorinsideexecute_code()are stored forfetch_logs()fetch_logs()drains the log buffer on read
execute_code
execute_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;With in-memory session storage:
await jsmcp.memory.set("search_results", await search.query({ q: "example" }));
return await jsmcp.memory.keys();A later execute_code() call in the same profile and session can read it:
const results = await jsmcp.memory.get("search_results", []);
return results.slice(0, 3);Available methods are get(key, defaultValue?), set(key, value), has(key), delete(key), keys(), entries(), clear(), and update(key, updater). Stored values are converted with the same JSON-safe conversion used for execute_code() results. Memory is in-process only and is cleared when jsmcp exits.
Results and execution time
Successful execute_code responses always put the JSON-safe return value in structuredContent.value, including objects, arrays, strings, and primitives. Returning nothing produces value: null. For example, return await math.add({ a: 2, b: 5 }); produces:
{
"value": { "sum": 7 },
"executionMs": 12
}executionMs is elapsed wall-clock time in rounded milliseconds, measured around runtime execution (including awaited tool calls and result conversion, but not response formatting or transport). Execution errors use { "error": "message", "executionMs": 12 } with isError: true, including syntax errors and timeouts.
Compatibility: object results used to appear directly in structuredContent. Callers must now read structuredContent.value instead. Other meta tools' response shapes are unchanged.
The optional boolean rawText defaults to true. When the returned value is a string, content[0].text contains that string verbatim, without JSON quoting or escaped newlines. A separate text block reports Execution time: <n>ms. The structured envelope is still provided for programmatic callers.
For example, call:
{
"code": "return data.message;",
"data": { "message": "Hello\nworld" }
}The first text block is:
Hello
worldPass rawText: false to render the entire { value, executionMs } envelope as JSON text instead. Non-string results always use JSON text. Errors retain plain error text with a separate timing block. These options behave identically over stdio, WebSocket, and HTTP. Clients that display structuredContent rather than content may still show JSON.
Connected MCP tool results
Calls inside your JavaScript are automatically unwrapped; agents do not need to access .structuredContent themselves:
If an upstream tool returns
structuredContent, the call resolves to that object.Otherwise, text-only content resolves to a string (multiple text blocks are joined with newlines). JSON-looking text is not automatically parsed.
Otherwise, mixed/non-text content resolves to
{ text, content, meta }, preserving the content blocks.Upstream
isErrorresults throw an error.
const result = await math.add({ a: 2, b: 5 });
return result.sum; // 7, not result.structuredContent.sumOnly the outer execute_code response adds { value, executionMs }; upstream objects are not otherwise flattened. To display a string field directly, return that field rather than its containing object.
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 });This server cannot be deployed
Maintenance
Related MCP Connectors
The MCP server that finds MCP servers. Aggregates Official Registry, Glama, and Smithery.
MCP server for progressive tool usage at any scale (see https://klavis.ai)
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Search, vet & assemble MCP servers from your agent: verified tools, risk labels, and trust scores.
Related MCP Servers
- AlicenseBqualityAmaintenanceA meta-MCP server that manages and aggregates other MCP servers, enabling LLMs to dynamically extend their own capabilities by searching for, adding, and configuring tool servers.1657 PyPI143AGPL 3.0
- AlicenseNot gradedqualityDmaintenanceA Model Context Protocol server manager that acts as a proxy/multiplexer, enabling connections to multiple MCP servers simultaneously and providing JavaScript code execution with access to all connected MCP tools. Supports both stdio and HTTP transports with OAuth authentication, batch tool invocation, and dynamic server management.15 npmMIT
- AlicenseAqualityAmaintenanceA meta-MCP server that acts as a single connection point to lazily spawn and proxy multiple MCP servers, reducing context bloat and process overhead.8MIT
- FlicenseAqualityDmaintenanceA meta-MCP server that orchestrates tools from multiple MCP servers, enabling complex Python workflows with loops and conditionals.2-