MCP Decoy
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., "@MCP Decoyecho back 'hello' so I can verify my client is connected"
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.
MCP Decoy
MCP Decoy is a local tool that hosts mock MCP servers for debugging MCP clients. You define tools in a web UI, connect your client to MCP Decoy, and watch every JSON-RPC message that the client sends and receives.
MCP Decoy is the counterpart of the MCP Inspector. The Inspector is a client that connects to your server. MCP Decoy is a server that your client connects to.
Use MCP Decoy when you build or debug an MCP client and need to answer questions like the following:
What does my client send during
initialize, and what does it do with the server's response?Does my client refresh its tool list after
notifications/tools/list_changed?How does my client handle a tool that returns
isError: true, a long text result, or an unusual input schema?What does a hosted client, such as a chat product that connects to remote MCP servers, send to my server?
Does my client complete an OAuth 2.1 authorization flow?
You can change tools while the client stays connected. MCP Decoy doesn't restart, and your client doesn't reconnect.
Requirements
Node.js 24 or later
Related MCP server: mcp-chaos-rig
Start MCP Decoy
Run MCP Decoy with npx:
npx @hodor-ai/mcp-decoyTo install MCP Decoy globally and run it as mcp-decoy:
npm install --global @hodor-ai/mcp-decoy
mcp-decoyThe output is similar to the following. The version and the config path differ on your machine.
┌ MCP Decoy v1.2.0
│
◆ UI http://localhost:6281
│
● API http://localhost:6281/api/v1
│
● Config /home/dana/.config/mcp-decoy/servers.json
│
● Servers
│ echo http://localhost:6280/s/echo/mcp
│
│ Press Ctrl+C to stop.MCP Decoy hosts mock MCP servers, called servers in this document. Each server
has its own tools, logs, and URL. One MCP Decoy process hosts any number of
servers. By default, MCP Decoy listens on port 6280 for MCP clients and on
port 6281 for the UI and the API.
If the config directory has no servers.json, MCP Decoy creates one server
named echo with one published tool, also named echo. The tool returns its
message argument. Open the UI URL from the output to manage servers and tools.
Connect your MCP client
Every server has its own URL, http://localhost:6280/s/SERVER_NAME/mcp. MCP
Decoy supports the Streamable HTTP transport, so give your client that URL and
select an HTTP transport. The following examples connect to the default echo
server on the default MCP port. To change the port, see Options.
Claude Code
Add the server with the claude mcp add command:
claude mcp add --transport http echo http://localhost:6280/s/echo/mcpCursor
Add the server to .cursor/mcp.json in your project, or to ~/.cursor/mcp.json
for all projects:
{
"mcpServers": {
"echo": { "url": "http://localhost:6280/s/echo/mcp" }
}
}Visual Studio Code
Add the server to .vscode/mcp.json:
{
"servers": {
"echo": { "type": "http", "url": "http://localhost:6280/s/echo/mcp" }
}
}Connect a client on another host
A client that runs on a remote host, such as a hosted chat product, can't reach
localhost on your machine. MCP Decoy serves plain HTTP, so put a tunnel in
front of the MCP port, 6280 by default.
Don't tunnel the UI port. The UI listener rejects any Host header other than
localhost or 127.0.0.1, so a tunnel to it returns 403 Forbidden.
To expose the MCP port, run a quick tunnel with cloudflared:
cloudflared tunnel --url http://localhost:6280Give the client the tunnel URL with the server path appended:
https://TUNNEL_HOST/s/echo/mcp. Replace TUNNEL_HOST with the host name that
cloudflared prints. In the UI header, select Need a public URL? to see
this command with your MCP host and port, and a button that copies it.
Check the connection
Each client connection is a session. The server assigns the session an ID, and
the client sends it in the Mcp-Session-Id header. To check that your client
connected, take the following steps:
In the UI, open the Servers page. After the client connects, the Sessions column of its server shows
1 live.Select the server, and then select the Connections tab. The Sessions section lists each session with the client name, the client version, and the session ID.
Select the Logs tab. It shows the
initializerequest and its response, followed by the requests that your client sends next, such astools/list.Call the
echotool from your client. The Logs tab shows thetools/callrequest and its response.Optional: To test how your client recovers from a closed session, go to the Connections tab and click Close on the session's row. You can also close a session with the
DELETE /servers/{server}/sessions/{session}endpoint of the REST API.
Define a tool
A tool is what a client lists and calls on a server. To create one, open a server in the UI, go to the Tools tab, and click New tool. A tool has the following fields:
Name: the name that clients use in
tools/call. The name is unique on the server and contains 1-128 letters, digits, underscores, dashes, or dots.Title: an optional display name.
Description: the text that clients, and the models behind them, read to decide when to call the tool.
Input schema: a valid JSON Schema that describes the arguments, with
"type": "object". MCP Decoy doesn't validate call arguments against it.Annotations: the
readOnlyHint,destructiveHint, andopenWorldHintcheckboxes. Clients receive them intools/list.Response template: what the tool returns. See Write a response template.
A tool is either a draft or published, and clients see only published tools. A new tool starts as a draft. Save draft keeps a tool hidden while you work on it, and Publish tool makes it visible. On a published tool, Save changes updates it and Unpublish hides it again.
MCP Decoy sends notifications/tools/list_changed to every connected session of
the server in the following cases:
You publish a tool.
You edit, rename, unpublish, or delete a published tool.
Changes to a draft don't send a notification. Use these notifications to test whether your client refreshes its tool list.
Write a response template
A response template defines the result of a tool call. MCP Decoy renders the
template with json-e on every tools/call, and the
output must be a valid MCP CallToolResult.
A template is JSON. Strings can interpolate values with ${...}. The only
variable that MCP Decoy provides is args, the arguments object of the call.
json-e also provides built-in functions and the now variable.
The following examples assume an input schema with a string property named
city.
The following template returns a sentence that includes an argument:
{
"content": [
{ "type": "text", "text": "Forecast for ${args.city}: 14°C, light rain" }
]
}To insert a value instead of text, use $eval. To convert a value to JSON text,
use $json. The following template, which is the default for new tools, returns
the arguments as JSON text:
{ "content": [{ "type": "text", "text": { "$json": { "$eval": "args" } } }] }To vary the result, use json-e operators such as $if. The following template
returns a tool error when city is Atlantis:
{
"content": [
{
"type": "text",
"text": {
"$if": "args.city == 'Atlantis'",
"then": "City not found: ${args.city}",
"else": "Forecast for ${args.city}: 14°C, light rain"
}
}
],
"isError": { "$eval": "args.city == 'Atlantis'" }
}The following rules apply:
json-e treats every object key that starts with
$as an operator. To use a key that starts with a literal$, double it:$$namebecomes$name.In a string,
$is literal unless{follows it. To write a literal${, write$${.The UI checks the template while you type. It renders the template with empty arguments and reports only errors that no arguments can fix, such as a syntax error. Errors that depend on
args, such as a missing property, appear when a client calls the tool. The UI doesn't check that the output is a validCallToolResult. That check runs when a client calls the tool.If a template fails to render, or the output isn't a valid
CallToolResult, the tool call returnsisError: trueand a text message. The message starts withMCP Decoy: the response template of this tool failed to render.and continues with the error details, one per line.
For the full operator list, see the json-e documentation.
Read the logs
The Logs tab records everything that reaches the MCP listener for the server, on two layers:
The http layer has one entry for each HTTP request, including OAuth requests and event streams. MCP Decoy doesn't log
OPTIONSpreflight requests.The mcp layer has one entry for each JSON-RPC message, including notifications and malformed bodies. A request and its response share one entry.
Each entry shows the time, the layer, the kind, the name, the status, a short summary, the session, and how long the exchange took.
In the Logs tab, you can do the following:
Select an entry to open its details: the headers, the query parameters, and the pretty-printed JSON of each message, with a button that copies it. The details link an
mcpentry to thehttpentry that carried it.Type in the search box to filter. The filter is a case-insensitive substring match over the method, the URL, the headers, and the bodies. The entry count reflects the filter.
Click Pause to read without the list moving, and Resume to catch up.
Click Clear to delete the entries of the server.
Click Export JSONL to download every entry of the server as newline-delimited JSON, whatever the filter.
MCP Decoy keeps logs in memory only, so they're gone when MCP Decoy exits. The
buffer holds the newest 10,000 log records across all servers. A log record is
one HTTP exchange, one JSON-RPC message, or the end of an event stream, so a
single tool call over HTTP uses at least three records. When the buffer is full,
MCP Decoy drops the oldest records first. To change the limit, use
--log-limit.
Test an OAuth client
A server can require OAuth 2.1, so you can test how your client discovers an authorization server, registers, asks for consent, and handles tokens. MCP Decoy acts as both the resource server and the authorization server. It supports the following standards:
Protected Resource Metadata (RFC 9728)
Authorization Server Metadata (RFC 8414)
Dynamic Client Registration
Proof Key for Code Exchange (PKCE), with the
S256method onlyResource Indicators (RFC 8707). The authorization request must include
resource.
Turn on OAuth
In the Connections tab of the server, turn on the switch in the OAuth section. You can also select OAuth 2.1 under Authentication when you create or edit the server.
Turning OAuth on generates a client ID and a client secret for the pre-registered client. Turning OAuth off asks for confirmation, then deletes those credentials.
Authorize a client
The handshake has the following steps:
The client sends a request without a token. MCP Decoy answers
401with aWWW-Authenticateheader that points to the protected-resource metadata.The client reads the metadata to find the authorization server, then registers itself or uses the pre-registered client.
The client sends the browser to the MCP Decoy consent page. Click Approve to redirect the browser to the client's redirect URI with an authorization code. Click Deny to redirect with
error=access_denied.The client exchanges the code for tokens and retries the request with the access token.
To watch the whole handshake, open the Logs tab. It lists the OAuth HTTP requests together with the JSON-RPC messages.
Access tokens expire after one hour. Refresh tokens don't expire, and each refresh issues a new token pair.
Use the pre-registered client
Each server on OAuth has one pre-registered client that has a secret. The OAuth section of the Connections tab shows its client ID and secret.
The client works only after you add redirect URIs in the Redirect URIs field of the same section and click Save redirect URIs. With no redirect URIs, only Dynamic Client Registration works.
The client authenticates with client_secret_post. Send client_id and
client_secret in the body of the token request. MCP Decoy doesn't support
Authorization: Basic.
Revoke tokens
The Tokens section of the Connections tab lists every live access and refresh token with its client, scopes, and expiry. Use the following buttons:
Revoke on a row invalidates one token.
Revoke all invalidates every token of the server.
After you revoke an access token, the client's next request gets a 401, and
the client must authorize again. If you revoke only a refresh token, the access
token stays valid, and the refresh request fails.
Changing the redirect URIs, turning OAuth on, and turning OAuth off also revoke every token and close the live sessions of the server.
Use HTTPS or a public URL
MCP Decoy builds the issuer from the address that your client uses to reach the
MCP port. The issuer must use https:, or http: with the host localhost or
127.0.0.1. A client that connects over plain HTTP to a LAN address, such as
when you start MCP Decoy with --host 0.0.0.0, can't complete OAuth.
Use a localhost URL, or put a tunnel in front of the MCP port and use the
HTTPS URL of the tunnel. For the command, see
Connect a client on another host.
OAuth state and restarts
MCP Decoy keeps OAuth state in memory only, like the logs. A restart discards
registered clients, authorization codes, and tokens, so clients must register
and authorize again. The pre-registered client persists in servers.json,
including its secret.
Options
Pass options after the command, as in mcp-decoy --mcp-port 7000. The following
table lists the options:
Option | Description | Default |
| Port of the MCP listener. |
|
| Port of the UI and API listener. |
|
| Bind address of the MCP listener. The UI listener always binds |
|
| Config directory, which holds |
|
| Log records kept in memory, across all servers. The minimum is |
|
| Print the usage text. | None |
| Print the version. | None |
If XDG_CONFIG_HOME isn't an absolute path, MCP Decoy ignores it and uses
~/.config/mcp-decoy.
If a port is in use, MCP Decoy exits and names the option to change.
Each config directory supports one running instance. If you start a second
instance with the same config directory, MCP Decoy exits and prints the URL and
the process ID of the running one. To run two instances, give each its own
--config-dir and ports.
Where MCP Decoy stores data
MCP Decoy writes servers.json to the config directory after every change. The
file holds your servers, their tools, and the pre-registered OAuth client of
each server that uses OAuth, including its client secret. You can back it up or
copy it to another machine. Before you commit it, remove the auth entries or
confirm that the secrets are throwaway test values.
If the file is missing, MCP Decoy creates it with the echo server. If the file
is invalid, MCP Decoy doesn't start and prints the file path and the problems.
While MCP Decoy runs, it also writes runtime.json to the same directory. The
file records the process ID, the version, the start time, the API URL, and the
MCP URL pattern. MCP Decoy uses it to detect a second instance, and removes it
when it shuts down cleanly.
REST API
The UI uses a REST API that you can call too. The API listens on the UI port,
under /api/v1. Request and response bodies are JSON, except for the log stream
and the log export, and errors use
RFC 9457 problem details.
The API accepts only requests addressed to localhost or 127.0.0.1 on the UI
port. A POST or PATCH request must send Content-Type: application/json,
and a browser request must come from the UI's own origin. Otherwise, the API
answers 415 or 403.
The paths in the following table are relative to /api/v1. Replace {server},
{tool}, {session}, {token}, and {id} with the name or ID of the
resource.
Method and path | Description |
| Get the version, listener ports, MCP URL template, and config file path. |
| List servers. |
| Create a server. |
| Get a server. |
| Update a server. A new |
| Delete a server and close its sessions. |
| List tools, drafts included. |
| Create a tool. |
| Get a tool. |
| Update a tool. Set |
| Delete a tool. |
| List live sessions. |
| Close a session. |
| List the live access and refresh tokens of a server that uses OAuth. |
| Revoke every access and refresh token of a server that uses OAuth. |
| Revoke one token, by the |
| List log entries, newest first, with cursor pagination. |
| Get one log entry by its numeric |
| Clear the logs of a server. |
| Stream log events as server-sent events. Pass |
| Download all entries as newline-delimited JSON. |
The following command publishes a draft tool named get_forecast on the
weather server:
curl -X PATCH http://localhost:6281/api/v1/servers/weather/tools/get_forecast \
-H 'Content-Type: application/json' \
-d '{"published": true}'MCP Decoy sends notifications/tools/list_changed to every live session of the
weather server.
The Zod schemas in src/shared/api/ define the request and response bodies.
Debug your client with a coding agent
MCP Decoy ships an agent skill that teaches a coding agent, such as Claude Code or Cursor, to drive MCP Decoy for you. With the skill installed, you can ask the agent to debug your client: it starts MCP Decoy, defines the tools that a scenario needs, runs your client, reads the logs, and reports what your client did wrong. For example, "my client never shows tools that I add at runtime. Use MCP Decoy to find out why."
To install the skill for all your projects, run the following command:
npx @hodor-ai/mcp-decoy skill installThe command writes ~/.agents/skills/mcp-decoy/SKILL.md and links
~/.claude/skills/mcp-decoy to it, so both Claude Code and the agents that read
.agents/skills find the skill. To install the skill into one project instead,
run the command from the root of the project with --dir .:
npx @hodor-ai/mcp-decoy skill install --dir .--dir is the directory that holds .agents/ and .claude/. It defaults to
your home directory.
Run the command again after you upgrade MCP Decoy to refresh the skill. To read
the skill, or to put it somewhere else yourself, run mcp-decoy skill. It
prints the skill to standard output.
The skill uses the REST API, so it works with any instance of MCP Decoy that your agent can reach on the API port.
Develop MCP Decoy
MCP Decoy needs Node.js 24 or later and pnpm. Run the following commands from a clone of the repository:
pnpm install
pnpm devpnpm dev starts the server and Vite. Open http://localhost:5173. In
development, only Vite serves the UI, and it proxies API requests to the server
on port 6281. MCP Decoy also stores its data in .mcp-decoy/ in the
repository, instead of the default config directory.
The following scripts run the same checks as CI:
Script | Description |
| Check formatting with oxfmt. |
| Run oxlint. Warnings fail the run. |
| Check types with |
| Run the Vitest unit and integration tests. |
| Build the UI and the CLI into |
The following scripts help while you work:
Script | Description |
| Format and lint with automatic fixes. |
| Format with oxfmt. |
| Run oxlint with automatic fixes. |
| Run Vitest in watch mode. |
| Run Vitest with a coverage report. |
| Run the built CLI, |
After pnpm build, run pnpm start to start the built package.
Install the agent skills
The repository records the agent skills that AI coding assistants use when they
work on MCP Decoy, such as the Hono and React guidance, in skills-lock.json.
Git ignores the skills themselves, so a fresh clone doesn't contain them.
To install them, run the following command from the root of the repository:
npx skills update -pThe -p flag limits the command to project skills, so it leaves your global
skills alone. The command downloads each skill in skills-lock.json from its
source repository into .agents/skills/ and links it into .claude/skills/.
The command installs the latest version of each skill from its source
repository, not the content that skills-lock.json recorded. It updates the
hashes in skills-lock.json to match. Review the change with
git diff skills-lock.json before you commit it.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for progressive tool usage at any scale (see https://klavis.ai)
Build your own tools from chat: design, test and host them on a shareable MCP server URL.
Related MCP Servers
- AlicenseBqualityDmaintenanceA mock MCP server for testing MCP client implementations and development workflows. Supports tools, prompts, and resources across multiple transport protocols (stdio, HTTP, SSE).1MIT
- AlicenseNot gradedqualityCmaintenanceA local MCP server that breaks on demand, allowing you to test your client against auth failures, disappearing tools, flaky responses, and token expiry from a web UI.6 npm10MIT
- AlicenseNot gradedqualityDmaintenanceA local mock MCP server for building against the Model Context Protocol. Write a tool in the browser, save it, and it is immediately available on an MCP endpoint.MIT
- AlicenseNot gradedqualityDmaintenanceA lightweight mock MCP server for local testing and resilience experiments, providing predictable tool responses with simulated latency and errors.1MIT