MCP server for Layers — connect Claude, Cursor, Windsurf to your workspace (120 tools)
8.4K
A Model Context Protocol server for Layers. Connect AI assistants like Claude, Cursor, and Windsurf directly to your Layers workspace — manage projects, tasks, pages, and more through natural language.
Protocol: MCP Streamable HTTP (2025-06-18 spec) | Tools: 120 | Transport: HTTP
layers_ws_...) — it is shown only oncePick your client below. Replace YOUR_DOMAIN with your Layers instance URL and YOUR_TOKEN with the token from step 1.
claude mcp add-json layers '{
"type": "http",
"url": "https://YOUR_DOMAIN/mcp",
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}'
Verify: claude mcp list — should show layers with 120 tools.
Requires: Node.js 18+ — run
node --versionto check.
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"layers": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"https://YOUR_DOMAIN/mcp",
"--header",
"Authorization:Bearer YOUR_TOKEN"
]
}
}
}
Important: After saving the config, fully quit Claude Desktop (Cmd+Q on macOS, Alt+F4 on Windows) and reopen it. Simply closing the window is not enough.
Node.js not found? If you use nvm/fnm, Claude Desktop may not see your Node. Use the full path: replace
"npx"with the output ofwhich npx(e.g.,/Users/you/.nvm/versions/node/v22.0.0/bin/npx).
Add to .cursor/mcp.json in your project root:
{
"mcpServers": {
"layers": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"https://YOUR_DOMAIN/mcp",
"--header",
"Authorization:Bearer YOUR_TOKEN"
]
}
}
}
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"layers": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"https://YOUR_DOMAIN/mcp",
"--header",
"Authorization:Bearer YOUR_TOKEN"
]
}
}
}
Add to .vscode/mcp.json in your workspace:
{
"servers": {
"layers": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"https://YOUR_DOMAIN/mcp",
"--header",
"Authorization:Bearer YOUR_TOKEN"
]
}
}
}
Go to Claude.ai > Settings > Integrations > Add custom integration:
| Field | Value |
|---|---|
| Name | Layers |
| Remote MCP server URL | https://YOUR_DOMAIN/mcp |
Enter your API token when prompted.
Add the MCP server to your Layers Docker Compose stack.
If you are standing up the full self-hosted Layers bundle — the Layers server, this MCP server, and the Layers AI Chat — this service is the middle layer. The request flow is:
browser → AI Chat → (internal network) MCP server :8091 → Layers REST API → Postgres
The chat (and any other in-cluster consumer) talks to the MCP server over the internal Docker network, sending each user's workspace token as a bearer:
LAYERS_MCP_URL = http://layers-mcp:8091/mcp
Internal endpoint vs. public endpoint.
http://layers-mcp:8091/mcpis the internal address, reachable only from inside the Docker network and authorized by the forwardedAuthorization: Bearer layers_ws_…. If you also expose/mcppublicly through your reverse proxy (for external AI assistants), that public URL normally sits behind your own SSO / proxy authorization and is a separate entry point. In-stack consumers like the chat must target the internallayers-mcp:8091address directly — not the public URL.
Issuing the workspace token the chat uses. The chat sends a Layers workspace
token as the bearer. Create it once in your Layers instance under
Settings > Access Tokens (it starts with layers_ws_… and is shown only once),
then hand it to the chat as described in its
hosting guide.
In the self-host bundle the image is pulled as
hissih/layers-mcp-server; the
env vars below (LAYERS_BACKEND_BASEURL, LAYERS_BACKEND_FORWARDINCOMINGAUTH, …) are
the same regardless of which tag you pull. Point LAYERS_BACKEND_BASEURL at the
Layers API over the internal network (e.g. http://layers-server:8080). The full
end-to-end stack assembly (domains, SSO cookie, reverse proxy, voice) is documented
in the chat repo — see its
README and
docs/hosting-guide.md.
Add this service alongside your existing layers-server and layers-db:
layers-mcp:
image: sinups/layers-mcp-server:latest
restart: always
depends_on:
- layers-server
environment:
LAYERS_BACKEND_BASEURL: http://layers-server:8080
LAYERS_BACKEND_FORWARDINCOMINGAUTH: "true"
LAYERS_BACKEND_REQUIREINCOMINGAUTH: "true"
LAYERS_BACKEND_BEARERTOKEN: ""
# Address of the web app as a person opens it — links in tool output come from here
LAYERS_BACKEND_FRONTENDBASEURL: https://app.example.com
LAYERS_MCP_ENDPOINT: /mcp
SERVER_PORT: "8091"
SPRING_PROFILES_ACTIVE: server
ports:
- "8091:8091"
networks:
- layers-internal
Users connect to https://YOUR_DOMAIN/mcp using their personal API tokens from Settings > Access Tokens.
If your Layers instance uses a reverse proxy (nginx, Traefik, Caddy), route /mcp to the MCP container on port 8091. Example for Traefik labels:
labels:
- traefik.enable=true
- "traefik.http.routers.layers-mcp.rule=Host(`YOUR_DOMAIN`) && PathPrefix(`/mcp`)"
- traefik.http.routers.layers-mcp.priority=200
- traefik.http.services.layers-mcp.loadbalancer.server.port=8091
With nginx:
location /mcp {
proxy_pass http://layers-mcp:8091;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_buffering off;
}
If you want to run the MCP server separately (not in the Layers Docker network):
docker run -d \
--name layers-mcp \
-p 8091:8091 \
-e LAYERS_BACKEND_BASEURL=https://YOUR_DOMAIN \
-e LAYERS_BACKEND_FORWARDINCOMINGAUTH=true \
-e LAYERS_BACKEND_REQUIREINCOMINGAUTH=true \
sinups/layers-mcp-server:latest
For personal use or testing, you can set a static token so clients don't need to provide their own:
docker run -d \
--name layers-mcp \
-p 8091:8091 \
-e LAYERS_BACKEND_BASEURL=https://YOUR_DOMAIN \
-e LAYERS_BACKEND_FORWARDINCOMINGAUTH=false \
-e LAYERS_BACKEND_REQUIREINCOMINGAUTH=false \
-e LAYERS_BACKEND_BEARERTOKEN="Bearer layers_ws_YOUR_TOKEN" \
sinups/layers-mcp-server:latest
Then connect clients to http://localhost:8091/mcp with no Authorization header needed.
| Environment Variable | Default | Description |
|---|---|---|
LAYERS_BACKEND_BASEURL | https://api.layers.md | URL of the Layers API |
LAYERS_BACKEND_BEARERTOKEN | (empty) | Static token for single-user mode |
LAYERS_BACKEND_FORWARDINCOMINGAUTH | true | Forward client's Authorization header to Layers |
LAYERS_BACKEND_REQUIREINCOMINGAUTH | true | Reject requests missing Authorization header |
LAYERS_MCP_ENDPOINT | /mcp | MCP endpoint path |
LAYERS_MCP_LOGTOOLCALLS | true | Log tool calls with arguments and duration |
SERVER_PORT | 8091 | HTTP port |
120 tools grouped by what they work on. Every tool carries MCP annotations (readOnlyHint, destructiveHint, idempotentHint), so a client can ask before anything destructive happens.
This list is generated from the server's own catalogue — see scripts/render-readme-tools.mjs.
| Tool | Description |
|---|---|
layers_access_users_detailed | Get detailed user list with roles |
layers_dashboard_activity | Get activity feed for a workspace or project |
layers_workspace_context | Get workspace overview: projects, members, recent pages (call first!) |
layers_workspace_items_hierarchy_tree | Get full workspace hierarchy (projects, folders) |
layers_workspace_members_list | List members of a workspace |
layers_workspace_resolve | Resolve a workspace by slug or ID |
layers_workspaces_list | List all workspaces the token has access to |
| Tool | Description |
|---|---|
layers_project_create | Create a new project |
layers_project_dashboard | Get project dashboard stats (task counts by status) |
layers_project_delete | Delete a project |
layers_project_get | Get a project by ID |
layers_project_get_raw | Get raw project data (unprocessed API response) |
layers_project_members | List members of a project |
layers_project_update | Update project name, description, or settings |
layers_projects_tree_by_workspace | List all projects in a workspace |
| Tool | Description |
|---|---|
layers_project_focus | Set, read or clear the project this conversation is working in |
| Tool | Description |
|---|---|
layers_sprint_create | Create a new sprint |
layers_sprint_delete | Delete a sprint |
layers_sprint_get | Get a sprint by ID |
layers_sprint_get_raw | Get raw sprint data |
layers_sprint_list | List sprints of a project |
layers_sprint_move | Move a sprint to another project |
layers_sprint_restore | Restore a deleted sprint |
layers_sprint_update | Update sprint name, dates, or goal |
| Tool | Description |
|---|---|
layers_milestone_create | Create a milestone |
layers_milestone_delete | Delete a milestone; the tasks that pointed at it simply lose it |
layers_milestone_get | Get a milestone by name or ID |
layers_milestone_list | List milestones of a project |
layers_milestone_update | Update a milestone — name, date, description |
| Tool | Description |
|---|---|
layers_comment_resolve | Mark a comment resolved — the tick people put on a thread once it is dealt with |
layers_task_activity | What happened to a task — its change history |
layers_task_assign | Assign or unassign a task |
layers_task_attachment_delete | Remove an attachment from a task |
layers_task_attachment_upload | Attach a file to a task; video goes through the chunked upload pipeline |
layers_task_attachments_list | List the files attached to a task |
layers_task_comment_create | Add a comment to a task |
layers_task_comment_delete | Delete a comment on a task |
layers_task_comment_update | Edit a comment on a task |
layers_task_comments_list | List comments on a task |
layers_task_create | Create a new task (auto-resolves projectName, assigneeName, statusName) |
layers_task_delete | Delete a task |
layers_task_get | Get a task by ID |
layers_task_get_raw | Get raw task data (unprocessed API response) |
layers_task_list | List tasks in a project; filters by assignee, status, due date are honoured |
layers_task_list_smart | Smart task list with natural-language filters (overdue, unassigned, mine) |
layers_task_priorities_list | List task priority levels |
layers_task_statuses_list | List available statuses for a project |
layers_task_types_list | List available task types for a project |
layers_task_update | Update task title, description, assignee, or status |
| Tool | Description |
|---|---|
layers_tag_create | Create a tag in a project |
layers_tag_create_or_get | Create a tag, or return the existing one with that name (safe to retry) |
layers_tag_delete | Delete a tag |
layers_tag_list | List tags of a project |
layers_tag_update | Rename a tag or change its colour |
| Tool | Description |
|---|---|
layers_convert_to_html | Convert markdown/plain text to rich HTML for page content |
layers_favorite_add | Add an item to favorites |
layers_favorite_remove | Remove an item from favorites |
layers_favorites_list | List favorite items |
layers_page_access_list | List who has access to a page |
layers_page_append | Append content to the end of a page without rewriting it |
layers_page_blocks | The exact HTML for every block a page can hold — call it while composing rich content |
layers_page_comment_create | Leave a comment on a page — markdown is accepted and arrives formatted |
layers_page_comments_list | The discussion on a page: comments with their reply threads |
layers_page_create | Create a new page with rich HTML content |
layers_page_delete | Delete a page |
layers_page_duplicate | Duplicate a page (with sub-pages) |
layers_page_get | Get a page by ID |
layers_page_get_plain_text | Get page content as plain text |
layers_page_get_raw | Get raw page data (unprocessed API response) |
layers_page_list_by_workspace | List pages across the workspace |
layers_page_move | Move a page to a different folder or project |
layers_page_publish | Publish a page (make publicly accessible) |
layers_page_remove_pin | Unpin a page |
layers_page_restore | Restore a deleted page |
layers_page_set_full_width | Switch a page between full width and centred |
layers_page_set_lock | Lock a page against editing |
layers_page_set_pin | Pin a page to the top of its list |
layers_page_settings | Read a page's settings — lock, width, pin, visibility |
layers_page_share | Share a page with specific users |
layers_page_unpublish | Unpublish a page |
layers_page_update | Update a page; pass the version you read to avoid overwriting someone else's edit |
layers_page_visibility | Change who can see a page |
| Tool | Description |
|---|---|
layers_folder_contents | List contents of a folder (pages, subfolders) |
layers_folder_create | Create a folder — inside a project or in the Cloud, whichever the parent is |
layers_folder_delete | Delete a folder |
layers_folder_get | Get a folder by ID |
layers_folder_get_raw | Get raw folder data |
layers_folder_restore | Restore a deleted folder |
layers_folder_update | Rename or move a folder |
| Tool | Description |
|---|---|
layers_form_brief | Turn a plain-language brief into a form draft |
layers_form_create | Create a new form |
layers_form_css_classes | List the style classes a form design can use |
layers_form_delete | Delete a form |
layers_form_design | Change a form's look — theme, colours, typography |
layers_form_get | Get a form by ID |
layers_form_get_raw | Get raw form data |
layers_form_list | List forms, including forms inside projects |
layers_form_media_search | Find images usable as form backgrounds |
layers_form_publish | Publish a form and get its public link |
layers_form_responses | Read the answers people submitted to a form |
layers_form_statistics | Response counts and completion rate for a form |
layers_form_templates | List ready-made form templates |
layers_form_update | Update form fields or settings |
| Tool | Description |
|---|---|
layers_item_resolve | Resolve any Layers item (task/page/project) from a URL or ID |
layers_search | Full-text search across tasks, pages, and projects |
| Tool | Description |
|---|---|
layers_navigate | Walk the hierarchy one level at a time: what is inside this thing? |
| Tool | Description |
|---|---|
layers_user_me | Get the currently authenticated user |
layers_user_resolve | Resolve a user by name, email, or ID |
| Tool | Description |
|---|---|
layers_invitation_accept | Accept a workspace invitation |
layers_invitation_delete | Revoke a pending invitation |
layers_workspace_invite | Invite a person to a workspace by email, with a role |
layers_workspace_kick | Remove a member from a workspace |
layers_workspace_usage_summary | Seats, storage and plan limits for a workspace |
| Tool | Description |
|---|---|
layers_comment_bulk_create | Post comments on many tasks in one call (rate-limited once for the batch) |
layers_comments_bulk_list | Read comments across many tasks in one call |
layers_form_bulk_delete | Delete several forms in one call |
layers_task_bulk_create | Create many tasks in one call |
layers_task_bulk_update | Update many tasks in one call |
| Tool | Description |
|---|---|
layers_context_acknowledge | Diagnostic: reflect back the context the server parsed from _meta |
| Tool | Description |
|---|---|
layers_toolsets | List the tool slices this server can expose, and their token cost |
Workspace Context — Call layers_workspace_context first to get a quick overview of projects, members, and recent pages. The AI assistant will know exactly where to create content.
Name Resolution — Use human-readable names instead of UUIDs:
"Create a task in project Design v2 and assign to Alice"
The server resolves projectName, assigneeName, statusName, and priorityName automatically.
Disambiguation — When a name matches multiple items, the server returns a helpful list:
Multiple projects match 'MCP'. Please be more specific:
- MCP Testing (id=b0f02a3d...)
- MCP Integration Test (id=dc399ea8...)
Use the exact name or provide the ID directly.
Auto-defaults — Missing fields get smart defaults:
Browse your workspace
"What workspaces do I have access to?"
Find overdue work
"Show me all tasks that are overdue"
Create a task
"Create a task called 'Fix login bug' in project Alpha and assign it to me"
Read a document
"Read the PRD page"
Check project health
"What's the progress on the Mobile App project?"
Plan a sprint
"List all unassigned tasks in the Backend project"
Manage team
"Who are the members of the Design project?"
cat your-config.json | python3 -m json.toolnode --version (need 18+)npx (see Claude Desktop section above)Symptom: Server connects, lists 120 tools, but tool calls return 401.
Cause: Your token is from a different Layers instance than the MCP server URL.
Fix: Make sure the MCP server URL and the token are from the same Layers instance. A token created on workspace-a.layers.md will not work with an MCP server pointing to workspace-b.layers.md.
Symptom: 401 Unauthorized on all requests.
Fix: Create a new token in Settings > Access Tokens. Tokens start with layers_ws_ and are shown only once.
Symptom: 403 with TOKEN_SCOPE_READ_ONLY on any create / update / assign /
comment / delete, while listing and search keep working.
Fix: The token was issued for reading only. Access level is chosen once, at creation, and cannot be changed afterwards — issue a new token in Settings > Access Tokens with Access: read and write and swap it into your client config. Retrying, or reaching for a different tool, will not help.
Symptom: Browser opens asking for OAuth login instead of using the token.
Fix: Clear the mcp-remote auth cache and restart:
rm -rf ~/.mcp-auth
Fix: Set a longer timeout with the MCP_TIMEOUT environment variable:
{
"mcpServers": {
"layers": {
"command": "npx",
"args": ["mcp-remote", "https://YOUR_DOMAIN/mcp", "--header", "Authorization:Bearer YOUR_TOKEN"],
"env": {
"MCP_TIMEOUT": "10000"
}
}
}
}
Add your CA certificate:
"env": {
"NODE_EXTRA_CA_CERTS": "/path/to/corporate-ca.crt"
}
npx mcp-remote https://YOUR_DOMAIN/mcp --debug
Logs are written to ~/.mcp-auth/.
AI Assistant (Claude / Cursor / Windsurf / VS Code)
|
| MCP Streamable HTTP (2025-06-18)
| Authorization: Bearer layers_ws_...
v
+------------------------------+
| Layers MCP Server | :8091/mcp
| Java 17 + Spring Boot 4 |
| 120 tools, 10 resources |
| 8 prompts, 8 completions |
+-------------+----------------+
| REST API + Bearer token (forwarded)
v
+------------------------------+
| Layers API | your-domain.com
+------------------------------+
Multi-user mode (default): Client token -> MCP Server -> Layers API (forwarded as-is)
Single-user mode: MCP Server static token -> Layers API (client sends no token)
./mvnw -DskipTests package
java -jar target/layers-mcp-server.jar \
--spring.config.location=file:./application-external.properties \
--spring.profiles.active=server
See deploy/application-external.properties.example for configuration template.
docker pull sinups/layers-mcp-server:latest
Available for linux/amd64 and linux/arm64.
Content type
Image
Digest
sha256:8606c3916…
Size
139.8 MB
Last updated
6 days ago
docker pull sinups/layers-mcp-server