# EVC Mesh MCP Server
[](https://spark.entire.vc/assets/evc-mesh-mcp?utm_source=github&utm_medium=readme)
> Tasks, comments, shared memory and handoffs for teams of people and AI agents, over MCP.
EVC Mesh is an open-source task manager built for teams where people and AI agents work side by side. This [Model Context Protocol](https://modelcontextprotocol.io/) server connects your agent to a [Mesh](https://github.com/entire-vc/evc-mesh) workspace: it can pick up assigned tasks, check out work, comment, move tasks through your workflow, hand off to teammates, and keep decisions in shared project memory.
Two tool profiles: `core` (26 tools, for everyday task work) and `full` (65 tools, including recurring tasks, docs and workspace admin). Every tool is marked read-only or write, so clients can ask before changing anything.
You need a Mesh workspace and an agent key from it. Mesh is self-hosted (Apache-2.0, `docker compose` setup in the [evc-mesh](https://github.com/entire-vc/evc-mesh) repository). Set `MESH_API_URL` to your instance and `MESH_AGENT_KEY` to the agent's key. Workspaces on the hosted instance are by invitation.
Works with Cursor, Cline, OpenClaw and other MCP clients.
**This is the actively developed copy.** [`evc-mesh`](https://github.com/entire-vc/evc-mesh) also ships an MCP server (`./cmd/mcp`, same `internal/mcp` tool set) that it builds and deploys itself — the two exist because Go's `internal/` visibility rules mean one repo can't import the other's package, not because they're meant to diverge. New tools and fixes land here first.
## Prerequisites
- Go 1.22+
- Running EVC Mesh instance
- Agent registered in Mesh with an API key (`agk_...`)
## Installation
```bash
go install github.com/entire-vc/evc-mesh-mcp@latest
```
Or build from source:
```bash
git clone https://github.com/entire-vc/evc-mesh-mcp.git
cd evc-mesh-mcp
go build -o evc-mesh-mcp .
```
### Docker
```bash
docker run -i --rm \
-e MESH_API_URL \
-e MESH_AGENT_KEY \
ghcr.io/entire-vc/evc-mesh-mcp
```
`-i` is required — the server speaks MCP over stdio, and Docker only wires up
stdin when the container runs interactively. Add `-e MESH_MCP_PROFILE=core`
to switch profiles (see [Tool Profiles](#tool-profiles) below). The image is
published for `linux/amd64` and `linux/arm64` from `Dockerfile` in this repo
on every tagged release (`docs/RELEASING.md`).
## Tool Profiles
The MCP server supports two profiles to optimize context window usage:
| Profile | Tools | Context overhead | Best for |
|---------|-------|-----------------|----------|
| **core** | 26 | ~8K tokens (4% of 200K) | Claude Code, Cursor, small-context models |
| **full** | 65 | ~18K tokens (9% of 200K) | Power users, automation agents, admin ops |
Set via `MESH_MCP_PROFILE` environment variable. Default: `full`.
## Configuration
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `MESH_API_URL` | Yes | `http://localhost:8005` | Base URL of the Mesh API |
| `MESH_AGENT_KEY` | Yes (stdio) | — | Agent API key (`agk_...`) |
| `MESH_MCP_PROFILE` | No | `full` | Tool profile for stdio: `core` or `full` (SSE serves both) |
| `MESH_MCP_TRANSPORT` | No | `stdio` | Transport mode: `stdio` or `sse` |
| `MESH_MCP_HOST` | No | `0.0.0.0` | SSE server bind host |
| `MESH_MCP_PORT` | No | `8081` | SSE server bind port |
| `MESH_MCP_AUTH_FAIL_RPM` | No | `20` | SSE mode: per-IP budget for authentication attempts against a not-yet-cached agent key on `/sse`, `/core/sse`, `/mcp`, `/mcp/core`. Over budget → `429` without calling Mesh API. `0` disables. |
| `MESH_MCP_SESSION_CACHE_TTL_MIN` | No | `15` | SSE mode: how long a successful authentication is trusted before the key is re-checked — bounds how long a revoked key keeps working without a restart. |
| `MESH_MCP_AUTH_FAIL_CACHE_SEC` | No | `30` | SSE mode: how long a failed authentication (bad/unknown key) is remembered, so repeating the same bad key doesn't call Mesh API every request. |
| `MESH_MCP_PUBLIC_URL` | No | — | SSE mode: the URL the MCP root is reachable at from outside, e.g. `https://mesh.example.com/mcp`. Used for the absolute SSE endpoint and as the OAuth resource URL (see [OAuth](#oauth-remote-connectors)). Unset: derived from each request's host, which is only right when clients reach this server directly — behind a proxy set it, or the derived core URL (`/core`) will not match the public one (`/mcp/core`). |
| `MESH_MCP_OAUTH_ISSUER` | No | origin of `MESH_MCP_PUBLIC_URL` | SSE mode: the OAuth authorization server named in the protected-resource metadata — your Mesh instance's public origin. Set it only if the MCP server is served from a different origin than the Mesh API. |
| `MESH_MCP_OAUTH_CACHE_TTL_SEC` | No | `60` | SSE mode: how long a verified OAuth access token is trusted before Mesh API is asked again. Deliberately much shorter than the agent-key TTL so a revoked grant stops working within about a minute. |
| `MESH_MCP_DECIDER_USERNAME` | No | — | Username recorded as `decided_by` when `record_owner_decision` answers a gated task. Unset: the workspace owner. |
| `MESH_MCP_LEGACY_TOOL_ALIASES` | No | off | `1` also registers the tools' earlier names, for deployments whose callers still use them. Leave off for new installs. |
### Running without credentials
In stdio mode the server also starts when `MESH_AGENT_KEY` is not set. It then
answers `initialize` and `tools/list` as usual, and every tool call returns
instructions for setting `MESH_API_URL` and `MESH_AGENT_KEY`. This lets MCP
clients and catalogs inspect the tool list before you have a key. If a key is
set but authentication fails at startup (API unreachable, key rejected), the
server keeps running: tools are listed, and each call retries authentication
and returns the reason until it succeeds.
### Tool annotations
Every tool declares the MCP hints `readOnlyHint`, `destructiveHint`,
`idempotentHint` and `openWorldHint`, so clients can tell read-only tools
(`get_*`, `list_*`, `recall`, `search_docs`, …) from ones that change or
remove data (`update_*`, `move_task`, `forget`, …).
### Client metrics
Each `initialize` is logged with the client's `clientInfo.name` and version,
and counted in the Prometheus metric `mesh_mcp_initialize_total{client,profile}`
(exposed on `/metrics` in SSE mode; client names are normalised and capped).
### Claude Code (stdio mode)
Add to your project's `.mcp.json`:
```json
{
"mcpServers": {
"evc-mesh": {
"command": "evc-mesh-mcp",
"env": {
"MESH_API_URL": "https://your-mesh-instance.example.com",
"MESH_AGENT_KEY": "agk_your-workspace_your-key",
"MESH_MCP_PROFILE": "core"
}
}
}
}
```
### Cursor
Add to Cursor MCP settings (Settings → MCP Servers):
```json
{
"evc-mesh": {
"command": "evc-mesh-mcp",
"env": {
"MESH_API_URL": "https://your-mesh-instance.example.com",
"MESH_AGENT_KEY": "agk_your-workspace_your-key",
"MESH_MCP_PROFILE": "core"
}
}
}
```
### SSE Mode (multi-agent, shared server)
For connecting multiple agents through a shared MCP endpoint:
```bash
MESH_API_URL=https://your-mesh-instance.example.com \
MESH_MCP_PORT=8081 \
evc-mesh-mcp --transport sse
```
SSE mode serves **two profiles simultaneously** on different paths:
| Path | Profile | Description |
|------|---------|-------------|
| `/sse` + `/message` | full | All 65 tools (backward compatible) |
| `/core/sse` + `/core/message` | core | 26 essential tools |
The same process also serves the **Streamable HTTP** transport (stateless, one
agent key per request, sent in the `Authorization: Bearer` or `X-Agent-Key`
header — the query parameter is refused there):
| Path | Profile |
|------|---------|
| `/mcp` | full |
| `/core` | core |
Authentication per connection via:
- `Authorization: Bearer agk_...` header
- `X-Agent-Key: agk_...` header
- `?agent_key=agk_...` query parameter (SSE connect only)
- `Authorization: Bearer mot_...` — an OAuth access token issued by your Mesh instance (see below); the header only, never the query string, and on the Streamable HTTP endpoints only (SSE connections need an agent key)
### OAuth (remote connectors)
Clients that sign users in with OAuth — remote-connector directories, MCP
Inspector, editors that follow the MCP authorization spec — connect to the
Streamable HTTP endpoints without a pre-shared key. Your Mesh instance is the
authorization server (dynamic client registration, PKCE, user consent); this
server is the resource server and does two things:
- **Challenges.** A request with no credential, or with an OAuth access token
Mesh API rejects (expired, revoked, never issued), gets `401` and
```
WWW-Authenticate: Bearer resource_metadata="https://mesh.example.com/.well-known/oauth-protected-resource/mcp", scope="mesh"
```
which is where a client starts the authorization flow. A rejected agent key
keeps answering `403`. Only a verdict from Mesh API (a `4xx` other than
`408`/`429`) makes a token invalid: if Mesh API cannot be reached or answers
with an error that says nothing about the token (`5xx`, `429`), the answer is
`503` with `Retry-After`, so a valid token is not thrown away over an outage.
- **Serves the metadata** ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728))
at the well-known path derived from each endpoint's URL:
| Endpoint | Metadata |
|----------|----------|
| `https://mesh.example.com/mcp` (full) | `/.well-known/oauth-protected-resource/mcp` |
| `https://mesh.example.com/mcp/core` (core) | `/.well-known/oauth-protected-resource/mcp/core` |
```json
{
"resource": "https://mesh.example.com/mcp",
"authorization_servers": ["https://mesh.example.com"],
"scopes_supported": ["mesh"],
"bearer_methods_supported": ["header"]
}
```
`resource` is `MESH_MCP_PUBLIC_URL` (trailing slash, query and fragment
removed; `/core` appended for the core profile), so set it to the URL your
users paste into the client.
The authorization server defaults to that URL's origin; override it with
`MESH_MCP_OAUTH_ISSUER`.
An OAuth token acts as a connector agent in the workspace the user chose when
they granted access, with that agent's permissions — the same tools, the same
permission model as an agent key. Verified tokens are cached for a minute (`MESH_MCP_OAUTH_CACHE_TTL_SEC`). The
per-IP budget (`MESH_MCP_AUTH_FAIL_RPM`) is spent by *rejected* tokens, not by
verifications, so many users behind one address are not throttled by their own
token refreshes. The token is not audience-bound: any valid access token from
your Mesh instance is accepted, which is the intent while the authorization
server and this server belong to the same deployment. Agent keys (`agk_...`) in `Authorization` or
`X-Agent-Key` work exactly as before, and stdio mode is unaffected.
Your reverse proxy must send `/.well-known/oauth-protected-resource*` to this
server instead of the web app's catch-all: a single-page app answers every
unknown path with `200 text/html`, which a client cannot tell from missing
metadata.
## Agent Context Protocol (ACP)
At session start, follow these 5 steps in order:
```
1. heartbeat(status="online") → register as alive
2. get_project_knowledge(project_id) → load accumulated decisions & conventions
3. get_my_rules(project_id) → understand constraints
4. get_context(project_id) → see recent activity + project knowledge
5. get_my_tasks() → check assigned work
```
At session end:
```
publish_event(type="summary", memory={persist: true}) → broadcast + persist
session_report(model, tokens_in, tokens_out) → report metrics
```
## MCP Tools — Core Profile (26)
### ACP & Identity
| Tool | Description |
|------|-------------|
| `heartbeat` | Send heartbeat. Call at session start with status=online. Response includes `mesh_version` (the running binary's build git-SHA, or `"dev"` for an unpinned local build) — cheap way to check whether a fix has actually reached the installed binary without shelling out to the host. |
| `get_project_knowledge` | Get ALL permanent knowledge (decisions, conventions). ACP Step 2 |
| `get_my_rules` | Get ALL governance rules (workflow + assignment). ACP Step 3 |
| `get_context` | Get recent activity + project knowledge. ACP Step 4 |
| `get_my_tasks` | Get assigned tasks with complete descriptions and optional lossless conditional responses. ACP Step 5 |
### Task Management
| Tool | Description |
|------|-------------|
| `list_projects` | List workspace projects |
| `list_tasks` | List tasks with filters (status, priority, assignee, search) |
| `get_task` | Get task details with optional comments/artifacts/deps |
| `create_task` | Create a new task |
| `update_task` | Update task fields |
| `move_task` | Change task status using slugs |
| `assign_task` | Assign/unassign a task |
| `get_task_context` | Get everything about a task in one call |
| `add_vcs_link` | Link a task to a pull request, commit or branch |
### Communication
| Tool | Description |
|------|-------------|
| `add_comment` | Add comment to a task (markdown). Response includes a `delivery` array per `@`-mention reporting whether it actually reached the recipient (task queue/notification) or was skipped/failed and why |
| `publish_event` | Publish event + optional memory hint for persistence |
### Memory
| Tool | Description |
|------|-------------|
| `recall` | Search memory by keywords (compact by default: ~300 chars of content per item; `full=true` for everything uncut) |
| `get_memory` | Full text of one memory by exact key |
| `remember` | Save knowledge (UPSERT by key) |
| `forget` | Delete a memory entry |
| `recall_with_graph` | Search memory, expanding results through the knowledge graph |
| `set_project_knowledge` | Write a structured project fact (upsert by key) |
| `get_canonical_updates` | Fetch canonical decisions recorded since a given time |
| `record_owner_decision` | Record a decision by the workspace owner as canonical project knowledge |
#### What `recall` guarantees about its result
**`limit` is a hard bound.** The response never contains more than `limit` items,
and `total` always equals the number of items actually returned. Nothing is added
to the page after it has been sized — not pinned rows, not graph-expanded
neighbours.
**Rows that fail `scope`/`tags`/`tags_any` are dropped, never returned unmarked.**
This holds regardless of how a row reached the result: ordinary retrieval, pinning,
or graph expansion. A pinned row is exempt from *ranking*, not from *eligibility* —
"pinned" means "do not let ranking bury this", not "show this to a caller who asked
for a different scope".
**Graph neighbours are marked and bounded.** With `RECALL_GRAPH_ENABLED=true`,
`recall` also runs a knowledge-graph expansion and folds in `hop > 0` neighbours,
each carrying `graph_boost: true` and `provenance: via:graph`. They occupy at most
`limit/4` of the page (at least 1 when `limit >= 2`, none when `limit < 2`) and take
its **tail** slots, displacing the weakest retrieval hits rather than being appended
on top. When expansion returns nothing usable, the page is exactly the base result —
the reserve is a ceiling, not a quota. `graph_boost_count` reports how many slots
were actually spent.
The reserve exists because base hits carry `score` (RRF across the retrieval arms)
and neighbours carry `composite_score` from a separate traversal — different fields
on different scales. Sorting the union on a common key does not balance them; in
practice every observed neighbour ranks below every base hit, so a naive merge-sort
would silently disable graph boost. The reserve makes that trade explicit and
tunable.
**Presets never overrule you.** `recall` classifies the query and may apply a
profile (e.g. multi-session widens the page). A profile only fills in parameters you
did not supply; an explicit `limit` always wins.
### Utility
| Tool | Description |
|------|-------------|
| `report_error` | Report an error on a task |
| `session_report` | Report session metrics (model, tokens, cost) |
## MCP Tools — Full Profile (adds 39 more, 65 total)
### Additional Task Tools
| Tool | Description |
|------|-------------|
| `get_project` | Get project details with statuses and custom fields |
| `create_subtask` | Create subtask under a parent (`status_slug` optional; defaults to the project's default status, not the parent's) |
| `add_dependency` | Add dependency between tasks |
| `remove_dependency` | Remove a dependency edge (`task_id` blocked, `depends_on_task_id` blocker; `reason` required and logged) |
| `checkout_task` | Atomic task lock for multi-agent coordination |
| `release_task` | Release atomic task lock |
| `extend_checkout` | Extend an existing task lock for longer-running work |
| `set_human_gate` | Freeze a task until a named person answers a recorded question (`addressee` must be a person; `supersedes` and `canonical_key` cover a repeat of an answered question) |
| `clear_human_gate` | Release a human gate |
### Comments & Artifacts
| Tool | Description |
|------|-------------|
| `list_comments` | List task comments (newest first by default) |
| `upload_artifact` | Upload file/code/log to a task |
| `list_artifacts` | List task artifacts |
| `get_artifact` | Get artifact details (`download_path`; bytes via the two-step download below) |
### Downloading an artifact
Downloading an artifact is two GETs. Step 1: GET /api/v1/artifacts//download with header X-Agent-Key: -> 200 JSON {"url": ""}. Step 2: GET that url with NO headers -> 200, the file bytes. Pitfalls: on step 1 only X-Agent-Key is accepted (X-API-Key and Authorization: Bearer give 401); on step 2 any extra header, Authorization in particular, breaks the presigned signature (400). The artifact's download_path is step 1's path. browser_only_url is a human page and answers 401 to an agent key by design.
### Event Bus
| Tool | Description |
|------|-------------|
| `publish_summary` | Publish work summary (convenience wrapper) |
| `subscribe_events` | Configure webhook delivery for events |
| `poll_tasks` | Long-poll for new task assignments |
### Agent & Team
| Tool | Description |
|------|-------------|
| `register_sub_agent` | Register a sub-agent |
| `list_sub_agents` | List sub-agents (optionally recursive) |
| `get_team_directory` | Get workspace team directory |
| `update_agent_profile` | Update agent role, capabilities, profile |
### Governance & Config
| Tool | Description |
|------|-------------|
| `get_project_rules` | Get all project rules |
| `get_assignment_rules` | Get assignment rules |
| `get_workflow_rules` | Get workflow rules with caller permissions |
| `import_workspace_config` | Import workspace config from YAML |
| `export_workspace_config` | Export workspace config as YAML |
### Recurring Tasks
| Tool | Description |
|------|-------------|
| `create_recurring_task` | Create recurring task schedule |
| `list_recurring_schedules` | List recurring schedules |
| `get_recurring_history` | Get instance history for a schedule |
| `trigger_recurring_now` | Trigger next instance immediately |
| `update_recurring_schedule` | Change or deactivate a recurring schedule |
| `delete_recurring_schedule` | Delete a recurring schedule (existing instances stay) |
### Documents & Knowledge
| Tool | Description |
|------|-------------|
| `list_docs` | List a project's documents (metadata only) |
| `get_doc` | Read a document (outline by default, body on request) |
| `search_docs` | Full-text search across a project's documents |
| `create_doc` | Create a document |
| `update_doc` | Edit a document (optimistic concurrency via `base_version`) |
| `comment_doc` | Comment on a document or a quoted passage |
| `list_doc_comments` | Read a document's comment threads |
| `get_canonical` | Query curated facts and decisions for a topic |
## Architecture
```
AI Agent (Claude Code / Cursor / Cline / OpenClaw)
↕ MCP (stdio or SSE)
EVC Mesh MCP Server (core or full profile)
↕ REST API (HTTP)
EVC Mesh API Server
↕
PostgreSQL / Redis / NATS / S3
```
The MCP server is a lightweight proxy — it translates MCP tool calls into REST API requests. No direct database access needed.
## Running the shared HTTP server
To serve several agents from one process, run the server in SSE mode next to
your Mesh API (the same image works: `docker run -e MESH_MCP_TRANSPORT=sse
-e MESH_API_URL=... -p 8081:8081 ghcr.io/entire-vc/evc-mesh-mcp`) and put it
behind your reverse proxy. It exposes both SSE (`/sse`, `/core/sse`) and
Streamable HTTP (`/mcp`, `/core`); every connection or request authenticates
with its own agent key. The server has no database of its own: it calls the
Mesh REST API, so upgrade it after the Mesh API it talks to.
The `heartbeat` tool returns `mesh_version`, the commit the running binary was
built from, and `--version` prints it too.
## Skills and plugin layout
The repo follows the [Open Plugins](https://agent-plugins.org) layout: `plugin.json` (manifest), `mcp.json` / `.mcp.json` (stdio server config, values come from your environment) and `skills/`:
| Skill | Purpose |
|-------|---------|
| [`mesh-tasks`](skills/mesh-tasks/SKILL.md) | Session start protocol, taking, updating and closing tasks |
| [`mesh-memory`](skills/mesh-memory/SKILL.md) | `remember` / `recall` / `forget` conventions |
| [`mesh-coordination`](skills/mesh-coordination/SKILL.md) | Team directory, locks, events, hand-offs (full profile) |
This server exposes tools only; it does not define MCP prompts or resources.
## Related
- [evc-mesh](https://github.com/entire-vc/evc-mesh) — Core platform (API + Web UI)
- [evc-mesh-openclaw-skill](https://github.com/entire-vc/evc-mesh-openclaw-skill) — OpenClaw skill (bash scripts)
Find us in other directories
[](https://lobehub.com/mcp/entire-vc-evc-mesh-mcp)
[](https://glama.ai/mcp/servers/entire-vc/evc-mesh-mcp)
[](https://smithery.ai/servers/entirevc/evc-mesh-mcp)
## License
[MIT](LICENSE)