Skip to content

feat(mcp): add a built-in MCP server for managing tasks - #3086

Draft
kchung wants to merge 4 commits into
generalaction:mainfrom
kchung:chung/feat-add-mcp-support
Draft

feat(mcp): add a built-in MCP server for managing tasks#3086
kchung wants to merge 4 commits into
generalaction:mainfrom
kchung:chung/feat-add-mcp-support

Conversation

@kchung

@kchung kchung commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Description

MCP support in Emdash: a local, token-protected MCP server that lets the agents Emdash runs create and manage tasks themselves. Emdash registers the server with the agents it manages, and it shows up in the MCP library as a managed entry.

Tool Params Description
list_projects none List open projects
list_tasks projectId, includeArchived? List tasks in a project with status; archived tasks are hidden unless asked for
create_task projectId, prompt?, name?, provider?, model?, branchName?, baseBranch?, chatUi?, autoApprove? Create a task in an isolated worktree; with a prompt, start an agent on it, otherwise leave it idle
rename_task projectId, taskId, name Rename a task
archive_task projectId, taskId Archive a task, keeping the worktree and branch
delete_task projectId, taskId, confirm? Delete a task; requires confirm: true when the worktree has uncommitted changes
run_task_script projectId, taskId, type Start a task's setup, run, or teardown worktree lifecycle script
stop_task_script projectId, taskId, type Stop a running lifecycle script started by run_task_script

This is a rewrite of #2938 against the current slice and Wire architecture. The tools live in src/core/features/mcp/node/server/ and reach hosts through the runtime broker, so they work the same for a local worktree and one on a remote workspace server.

The delete_task gate asks git on the owning host rather than the registry mirror's dirty flag, which is only as fresh as the last host scan. It pairs the status live state with a git diff, since the first can lag a just written file and the second never sees untracked files. Anything unreadable needs confirmation too, so the gate fails closed.

One new dependency, @modelcontextprotocol/sdk. Nothing in the existing stack implements the MCP wire protocol.

Related issues

Resolves #2937

Testing

  • pnpm run format, pnpm run lint, pnpm run typecheck, pnpm run test
  • New suites: HTTP server, tools, self-registration, task creation, controller redaction, icon resolver
  • Local: exercised the tools end to end in the dev app, including the dirty worktree confirm flow
  • Remote: drove the delete gate against a worktree in the Docker workspace-server, covering clean, untracked, a fresh edit, and a non-checkout path

Screenshot/Recording (if applicable)

Coming soon

Checklist
  • I kept this PR small and focused
  • I ran a self-review before opening this PR
  • I ran the relevant local checks or explained why not
  • I updated docs when behavior or setup changed
  • I added or updated tests when behavior changed, or explained why not
  • I only added comments where the logic is not obvious
  • I used Conventional Commits for commit
    messages and, when possible, the PR title

kchung and others added 4 commits August 31, 2026 10:52
…tion

Pull `resolveConversationRuntimeTarget` out of the conversations wire
controller into its own module, and move `generateTaskName` into the tasks
`api/` surface, so callers outside those controllers can reach both without
crossing a slice's `node/` boundary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Emdash now exposes its own MCP server so an agent can create and manage
Emdash tasks. Tools: list_projects, list_tasks, create_task, rename_task,
archive_task, delete_task, run_task_script, stop_task_script.

The server listens on 127.0.0.1:8212/mcp in streamable-HTTP stateless mode.
Every request needs a bearer token, generated on first start and persisted
0o600 under userData; Host and Origin must be loopback, which blocks DNS
rebinding from a browser. EMDASH_MCP_SERVER=false skips the listener,
EMDASH_MCP_PORT moves it.

Emdash appears first in the MCP catalog as a managed entry: the drawer only
asks which agents to sync to, and the node side fills in the URL and token
on save. "Managed" is derived from the entry's shape, so a server the user
happens to name emdash is left alone, and the entry is hidden for remote
hosts since the socket is on the desktop's loopback interface.

Task sessions live inside the project attachment, so create_task starts an
agent only when the app already holds the project open, and otherwise says
in its result why it did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of the MCP server found delete_task's safety gate reading
hasUncommittedChanges from getDeletePreflight, which derives it from the
workspace registry mirror ("from the mirror alone, no host round-trip").
Attaching the project starts observation but does not refresh the record, so
an agent that creates a task, writes files, and asks to delete it was checked
against an observation predating its own edits, and a never-scanned workspace
reported clean. The worktree then went away with no confirmation.

The gate now reads the checkout's git status through the host runtime at call
time and fails closed: a status it cannot read, or an oversized working set
git declines to enumerate, needs confirmation too. It reports live staged,
unstaged, untracked, and conflicted counts instead of stale mirror numbers,
and skips the check entirely when the delete leaves the worktree in place.

Also from the same review:

- fix the icon glob, which resolved to a directory that has not existed since
  the file moved, so every MCP catalog icon silently fell back to the default.
  Covered by a browser test that fails with the old pattern.
- scan a few ports past the default when it is taken, instead of leaving the
  server down and reporting "not running" while a foreign process owns the URL
  already written into agent configs. An explicit EMDASH_MCP_PORT still binds
  literally.
- answer immediately when a project's attachment has already failed, rather
  than waiting out the 60s timeout for an update that cannot come.
- mark truncated list_tasks output and drop archived tasks by default, so an
  agent does not read a capped list as proof a task is absent.
- correct two docs that overstated the code: the bearer token does leave its
  0600 file for each agent's config, and the self-server match ignores the
  port on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verifying the delete gate against a real remote workspace server turned up
two defects in it.

The status live state is recomputed by a filesystem watcher, so a snapshot
taken right after a write can still report clean. That is the exact race the
gate exists for. Pair it with getChangedFiles, a direct `git diff` with no
cache in front of it, and treat either read reporting work as dirty. Neither
read is sufficient alone: the diff never reports untracked files, and the
status state can lag.

A checkout that cannot be opened at all rejects rather than returning an
error state, so the documented "unreadable needs confirmation too" path was
unreachable and the caller got an internal error instead. Both reads now sit
inside a try.

The unit-test fake returned data where the real client throws, which is why
neither defect showed up in tests. It now throws for that case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat]: Add MCP support for emdash

1 participant