Content
# 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 `jsmcp` constrains access to whatever servers and tools you allow in a preset
- the 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
## Install
```bash
npm install -g @alesya_h/jsmcp
```
Or run it without installing globally:
```bash
npx @alesya_h/jsmcp run
```
## Run
```bash
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 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_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://<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, and accepts `--session-id <id>` to reuse the same daemon-side log session across client reconnects.
`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`.
The daemon also exposes the five meta tools through one JSON HTTP endpoint, plus a restart endpoint:
```text
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/restart
```
The `/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.
## systemd User Service
This repo includes `systemd/jsmcp.service`, a user unit that starts `jsmcp server` from the globally installed CLI.
Install it with:
```bash
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:
```bash
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`.
## Config
The config file may be JSON or YAML and uses these top-level keys:
- `servers`: server definitions
- `jsmcp`: optional jsmcp-specific settings
- `presets`: 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"` or `type: "stdio"`
- remote servers: `type: "remote"`, `type: "http"`, or `type: "sse"`
- commands: either `command: ["cmd", "arg1"]` or `command: "cmd"` with `args: ["arg1"]`
- environment variables: either `environment` or `env`
Supported `servers.<name>` fields:
- `type`: required; one of `local`, `stdio`, `remote`, `http`, `sse`
- `description`: optional string shown in `list_servers()`
- `enabled`: optional boolean; defaults to `true`
- `timeout`: optional number in milliseconds used for initial tool discovery
- `strip_tool_prefix`: optional string, `true`, or `false`; strings are removed from exposed tool names, `true` infers a shared prefix, and `false` disables prefix stripping for that server
- `normalize_tool_names`: optional boolean; converts exposed tool names to `snake_case` after prefix stripping
- `blocked_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; default `false`; if `true`, servers infer and strip shared tool-name prefixes unless overridden by `servers.<name>.strip_tool_prefix`
- `normalize_tool_names`: optional boolean; default `false`; if `true`, servers expose tool names as `snake_case` unless overridden by `servers.<name>.normalize_tool_names`
For local / stdio servers:
- `command`: required; non-empty string or non-empty array
- `args`: optional array; appended to `command` when `command` is a string, and also accepted when `command` is an array
- `env`: optional object of environment variables
- `environment`: optional object of environment variables; merged with `env`, and wins on duplicate keys
- `cwd`: optional working directory
For remote / HTTP / SSE servers:
- `url`: required string
- `headers`: optional object of request headers
- `oauth`: optional OAuth config
Supported `oauth` forms:
- omitted, `null`, or `true`: enable OAuth with default behavior
- `false`: disable OAuth for that server
- object with any of:
- `clientId`
- `clientSecret`
- `scope`
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 directory
- absolute 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 preset
- any 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 tools
- `false`: exclude that server from that preset
- `"tool_name"`: include only that exact tool
- array 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:
```json
{
"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"`, and `command` plus `args` are supported
- OpenCode-style `type: "local"`, `type: "remote"`, command arrays, and `environment` are also supported
- Claude Code-specific features such as `headersHelper` and advanced OAuth fields like `callbackPort` or `authServerMetadataUrl` are 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_servers`
- `list_tools`
- `execute_code`
- `fetch_logs`
- `clear_logs`
## Behavior
- every server with `enabled !== false` is started once when `jsmcp` starts
- `list_servers()` is the required first step so the agent can learn what capabilities are available
- you must call `list_tools(server)` before using a server in `execute_code()` so you know the exact tool names, aliases, and schemas
- `list_servers()` and `list_tools(server)` return only the servers and tools allowed in the connection's selected profile
- `execute_code({ code, data?, timeoutMs? })` does not manage server lifecycle; it can only use servers that are already started
- prefer `execute_code({ code, ... })` whenever the work would require more than a single tool call
- `console.log`, `console.info`, `console.warn`, and `console.error` inside `execute_code()` are stored for `fetch_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:
```js
return await math.add({ a: 2, b: 5 });
```
With data:
```js
return data.message;
```
If the MCP tool returns `structuredContent`, that is what the JavaScript call resolves to. So the example above can return:
```json
{
"sum": 7
}
```
If a tool name is not a valid JavaScript identifier, prefer its underscore alias:
```js
return await math.tool_name({ value: 1 });
```
The original tool name still works with bracket access:
```js
return await math["tool-name"]({ value: 1 });
```
Connection Info
You Might Also Like
everything-claude-code
Complete Claude Code configuration collection - agents, skills, hooks,...
markitdown
MarkItDown-MCP is a lightweight server for converting URIs to Markdown.
cc-switch
All-in-One Assistant for Claude Code, Codex & Gemini CLI across platforms.
servers
Model Context Protocol Servers
servers
Model Context Protocol Servers
Time
A Model Context Protocol server for time and timezone conversions.