feat(mcp): add a built-in MCP server for managing tasks - #3086
Draft
kchung wants to merge 4 commits into
Draft
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
list_projectslist_tasksprojectId,includeArchived?create_taskprojectId,prompt?,name?,provider?,model?,branchName?,baseBranch?,chatUi?,autoApprove?rename_taskprojectId,taskId,namearchive_taskprojectId,taskIddelete_taskprojectId,taskId,confirm?confirm: truewhen the worktree has uncommitted changesrun_task_scriptprojectId,taskId,typesetup,run, orteardownworktree lifecycle scriptstop_task_scriptprojectId,taskId,typerun_task_scriptThis 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_taskgate asks git on the owning host rather than the registry mirror'sdirtyflag, which is only as fresh as the last host scan. It pairs thestatuslive state with agit 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 testScreenshot/Recording (if applicable)
Coming soon
Checklist
messages and, when possible, the PR title