@subtask/mcp
v0.2.0
Published
Subtask MCP server: lets AI clients reach Subtask workspaces and issues with an API token
Maintainers
Readme
Subtask MCP server
A stdio server that lets AI clients (Claude Code, Claude Desktop, anything that speaks MCP) reach Subtask's workspace and issue endpoints with an API token.
This is direction 1: an outside AI connects to Subtask. Subtask is passive here — it calls no LLM and pays for nothing. Subtask triggering AI from the inside (direction 2) is separate work.
Setup
- Create a token in Subtask (Company → Integrations → API tokens). Scopes:
workspaces:read,issues:read,issues:write. - The token is shown once. Copy it.
- Add it to your MCP client's configuration:
{
"mcpServers": {
"subtask": {
"command": "npx",
"args": ["-y", "@subtask/mcp"],
"env": {
"SUBTASK_URL": "https://subtask.app",
"SUBTASK_TOKEN": "sbt_..."
}
}
}
}Claude Code needs no file editing:
claude mcp add subtask --env SUBTASK_URL=https://subtask.app --env SUBTASK_TOKEN=sbt_... -- npx -y @subtask/mcpOn Windows, if the client reports that it cannot find npx, use cmd as the
command and ["/c", "npx", "-y", "@subtask/mcp"] as the arguments — npx is a
.cmd shim there and some clients cannot spawn it directly.
Both snippets are also generated inside the Subtask interface
(client/src/integrations/ApiTokenHelp.js) with the address and token already
filled in. If the package name changes, update both places: the
MCP_PACKAGE constant there and the examples here.
To run a working copy instead of the published version during local
development: "command": "node", "args": ["<repo>/mcp-server/index.js"].
The token is read only from the environment; it is not a parameter of any tool. The model can neither see it nor copy it into a tool call.
| Variable | Required | Default |
| --- | --- | --- |
| SUBTASK_URL | yes | — |
| SUBTASK_TOKEN | yes | — |
| SUBTASK_TIMEOUT_MS | no | 30000 |
Tools
| Tool | Writes | Scope |
| --- | --- | --- |
| subtask_workspaces_list | no | workspaces:read |
| subtask_issue_statuses | no | workspaces:read |
| subtask_issues_list | no | issues:read |
| subtask_issue_get | no | issues:read |
| subtask_issue_create | yes | issues:write |
| subtask_issue_update | yes | issues:read + issues:write |
| subtask_issue_comment | yes | issues:write |
| subtask_issue_set_status | yes | issues:write (+ issues:read recommended) |
| subtask_issue_ai_solve | yes | ai:run |
| subtask_issue_link_labels | no | issues:read |
| subtask_issue_links | no | issues:read |
| subtask_issue_link | yes | issues:read + issues:write |
ai:run is a separate scope: it hands the issue to an AI workflow in GitHub
Actions, which spends the customer's Actions quota and their LLM key. That is
not an ordinary write, so it was not lumped in with issues:write.
Writing tools are marked with MCP annotations.readOnlyHint: false, so the
client can ask for confirmation.
Two traps this server absorbs
Both are real behaviours of the Subtask API; without the MCP layer an agent falls into each of them.
1. /issues/upsert does not patch, it replaces.
A field missing from the body becomes null. This never causes trouble in the
browser, because the UI always submits a fully populated edit form. But for an
agent, "update the description of this issue" is the most natural request, and
it would silently wipe the issue's workspace, assignee and dates.
That is why subtask_issue_update does read-merge-write: it fetches the
issue first, changes only the given fields, and sends a complete body to the
endpoint. This is also why issues:read is mandatory for that tool.
2. Failure is reported with HTTP 200.
Subtask returns business rule errors as
{"status": null, "values": {"message": "..."}} — with HTTP status still 200.
The assumption "it returned 200, so it worked" breaks here. Every response goes
through unwrap and failures are turned into MCP isError responses, so the
model does not quietly assume success; it can read the message and correct
itself.
Tests
npm testtest/stdio.test.js starts a real process, speaks real JSON-RPC and connects to
a fake Subtask server — no manual verification needed.
Layers of protection
- Argument validation is derived from the tool schema and runs again on the
server before the call (
inputSchemais metadata; there is no guarantee the client enforces it). An unknown field or a wrong type never reaches the API. - A partial read blocks an update. If
/issues/getSingleever starts returning partial responses, merging would drop the missing fields; so when the issue that was read is incomplete, the tool stops loudly instead of writing data. - An empty success response is not treated as success.
subtask_issue_createerrors on a response that carries no id, so the model does not say "issue created" and carry on without one.
Known limits
- The server can only reach endpoints whose scope is declared in
app/docs/api-tokens.md. Opening a new endpoint takes two steps on the Subtask side (the scope, plusscopeon the route). - There are no tools for deleting issues, worklogs or labels; those endpoints are closed to tokens.
- Issue key generation (
key) is open to aMAX(key) + 1race: two issues created in the same workspace at the same time can get the same key. - The read-merge-write in
subtask_issue_updateis not atomic. A change made by someone else between the read and the write can be overwritten with the older snapshot. The API has no version/etag field, so there is no fix today. - If
subtask_issue_set_statuscannot read the issue, it still changes the status, but the response carries acache_context: "skipped: ..."field — list screens may show stale data for a while.
For maintainers
The setup text in the Subtask interface says npx -y @subtask/mcp, so the
package must be published on npm; until it is, the instructions on that
screen do not work.
cd mcp-server
npm test
npm publish # scoped package; publishConfig.access = public- The
@subtaskscope must belong to you on npm (npm org ls subtask, or the scope is created automatically on the first publish). - The
filesfield ships onlyindex.js+src;test/andnode_modules/stay out of the package. - Bump
versionafter every change —npxcaches by version number, and republishing the same version is not possible. licenseis currentlyUNLICENSED. Since the package sits publicly on npm, that means "you may read the source but you have no right to use it". A permissive licence (MIT, for example) would be more consistent if customers are meant to install it — a deliberate decision, which is why it was left alone.
