Sign inSign up

sinups/layers-mcp-server

By sinups

Updated 6 days ago

MCP server for Layers — connect Claude, Cursor, Windsurf to your workspace (120 tools)

Image
API management
0

8.4K

sinups/layers-mcp-server repository overview

Layers MCP Server

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.

Docker Hub

Protocol: MCP Streamable HTTP (2025-06-18 spec) | Tools: 120 | Transport: HTTP


Quick Start

1. Get an API Token
  1. Open your Layers workspace: Settings > Access Tokens
  2. Click Add access token, give it a name, set an expiry
  3. Set Access to read and write — a read-only token can list and search, but every create / update / assign / comment / delete is refused by the backend
  4. Copy the token (starts with layers_ws_...) — it is shown only once
2. Connect Your AI Assistant

Pick your client below. Replace YOUR_DOMAIN with your Layers instance URL and YOUR_TOKEN with the token from step 1.


Claude Code (CLI)
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.


Claude Desktop

Requires: Node.js 18+ — run node --version to 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 of which npx (e.g., /Users/you/.nvm/versions/node/v22.0.0/bin/npx).


Cursor

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"
      ]
    }
  }
}

Windsurf

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"
      ]
    }
  }
}

VS Code (Copilot)

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"
      ]
    }
  }
}

Claude.ai (Web)

Go to Claude.ai > Settings > Integrations > Add custom integration:

FieldValue
NameLayers
Remote MCP server URLhttps://YOUR_DOMAIN/mcp

Enter your API token when prompted.


Self-Hosted Deployment

Add the MCP server to your Layers Docker Compose stack.

Where it sits in the Layers self-host 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/mcp is the internal address, reachable only from inside the Docker network and authorized by the forwarded Authorization: Bearer layers_ws_…. If you also expose /mcp publicly 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 internal layers-mcp:8091 address 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.

Docker Compose

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.

Reverse Proxy

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;
}
Standalone Docker

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
Single-User Mode

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.


Configuration Reference

Environment VariableDefaultDescription
LAYERS_BACKEND_BASEURLhttps://api.layers.mdURL of the Layers API
LAYERS_BACKEND_BEARERTOKEN(empty)Static token for single-user mode
LAYERS_BACKEND_FORWARDINCOMINGAUTHtrueForward client's Authorization header to Layers
LAYERS_BACKEND_REQUIREINCOMINGAUTHtrueReject requests missing Authorization header
LAYERS_MCP_ENDPOINT/mcpMCP endpoint path
LAYERS_MCP_LOGTOOLCALLStrueLog tool calls with arguments and duration
SERVER_PORT8091HTTP port

Available Tools

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.

Workspaces (7 tools)
ToolDescription
layers_access_users_detailedGet detailed user list with roles
layers_dashboard_activityGet activity feed for a workspace or project
layers_workspace_contextGet workspace overview: projects, members, recent pages (call first!)
layers_workspace_items_hierarchy_treeGet full workspace hierarchy (projects, folders)
layers_workspace_members_listList members of a workspace
layers_workspace_resolveResolve a workspace by slug or ID
layers_workspaces_listList all workspaces the token has access to
Projects (8 tools)
ToolDescription
layers_project_createCreate a new project
layers_project_dashboardGet project dashboard stats (task counts by status)
layers_project_deleteDelete a project
layers_project_getGet a project by ID
layers_project_get_rawGet raw project data (unprocessed API response)
layers_project_membersList members of a project
layers_project_updateUpdate project name, description, or settings
layers_projects_tree_by_workspaceList all projects in a workspace
Focus (1 tool)
ToolDescription
layers_project_focusSet, read or clear the project this conversation is working in
Sprints (8 tools)
ToolDescription
layers_sprint_createCreate a new sprint
layers_sprint_deleteDelete a sprint
layers_sprint_getGet a sprint by ID
layers_sprint_get_rawGet raw sprint data
layers_sprint_listList sprints of a project
layers_sprint_moveMove a sprint to another project
layers_sprint_restoreRestore a deleted sprint
layers_sprint_updateUpdate sprint name, dates, or goal
Milestones (5 tools)
ToolDescription
layers_milestone_createCreate a milestone
layers_milestone_deleteDelete a milestone; the tasks that pointed at it simply lose it
layers_milestone_getGet a milestone by name or ID
layers_milestone_listList milestones of a project
layers_milestone_updateUpdate a milestone — name, date, description
Tasks (20 tools)
ToolDescription
layers_comment_resolveMark a comment resolved — the tick people put on a thread once it is dealt with
layers_task_activityWhat happened to a task — its change history
layers_task_assignAssign or unassign a task
layers_task_attachment_deleteRemove an attachment from a task
layers_task_attachment_uploadAttach a file to a task; video goes through the chunked upload pipeline
layers_task_attachments_listList the files attached to a task
layers_task_comment_createAdd a comment to a task
layers_task_comment_deleteDelete a comment on a task
layers_task_comment_updateEdit a comment on a task
layers_task_comments_listList comments on a task
layers_task_createCreate a new task (auto-resolves projectName, assigneeName, statusName)
layers_task_deleteDelete a task
layers_task_getGet a task by ID
layers_task_get_rawGet raw task data (unprocessed API response)
layers_task_listList tasks in a project; filters by assignee, status, due date are honoured
layers_task_list_smartSmart task list with natural-language filters (overdue, unassigned, mine)
layers_task_priorities_listList task priority levels
layers_task_statuses_listList available statuses for a project
layers_task_types_listList available task types for a project
layers_task_updateUpdate task title, description, assignee, or status
Tags (5 tools)
ToolDescription
layers_tag_createCreate a tag in a project
layers_tag_create_or_getCreate a tag, or return the existing one with that name (safe to retry)
layers_tag_deleteDelete a tag
layers_tag_listList tags of a project
layers_tag_updateRename a tag or change its colour
Pages (28 tools)
ToolDescription
layers_convert_to_htmlConvert markdown/plain text to rich HTML for page content
layers_favorite_addAdd an item to favorites
layers_favorite_removeRemove an item from favorites
layers_favorites_listList favorite items
layers_page_access_listList who has access to a page
layers_page_appendAppend content to the end of a page without rewriting it
layers_page_blocksThe exact HTML for every block a page can hold — call it while composing rich content
layers_page_comment_createLeave a comment on a page — markdown is accepted and arrives formatted
layers_page_comments_listThe discussion on a page: comments with their reply threads
layers_page_createCreate a new page with rich HTML content
layers_page_deleteDelete a page
layers_page_duplicateDuplicate a page (with sub-pages)
layers_page_getGet a page by ID
layers_page_get_plain_textGet page content as plain text
layers_page_get_rawGet raw page data (unprocessed API response)
layers_page_list_by_workspaceList pages across the workspace
layers_page_moveMove a page to a different folder or project
layers_page_publishPublish a page (make publicly accessible)
layers_page_remove_pinUnpin a page
layers_page_restoreRestore a deleted page
layers_page_set_full_widthSwitch a page between full width and centred
layers_page_set_lockLock a page against editing
layers_page_set_pinPin a page to the top of its list
layers_page_settingsRead a page's settings — lock, width, pin, visibility
layers_page_shareShare a page with specific users
layers_page_unpublishUnpublish a page
layers_page_updateUpdate a page; pass the version you read to avoid overwriting someone else's edit
layers_page_visibilityChange who can see a page
Folders (7 tools)
ToolDescription
layers_folder_contentsList contents of a folder (pages, subfolders)
layers_folder_createCreate a folder — inside a project or in the Cloud, whichever the parent is
layers_folder_deleteDelete a folder
layers_folder_getGet a folder by ID
layers_folder_get_rawGet raw folder data
layers_folder_restoreRestore a deleted folder
layers_folder_updateRename or move a folder
Forms (14 tools)
ToolDescription
layers_form_briefTurn a plain-language brief into a form draft
layers_form_createCreate a new form
layers_form_css_classesList the style classes a form design can use
layers_form_deleteDelete a form
layers_form_designChange a form's look — theme, colours, typography
layers_form_getGet a form by ID
layers_form_get_rawGet raw form data
layers_form_listList forms, including forms inside projects
layers_form_media_searchFind images usable as form backgrounds
layers_form_publishPublish a form and get its public link
layers_form_responsesRead the answers people submitted to a form
layers_form_statisticsResponse counts and completion rate for a form
layers_form_templatesList ready-made form templates
layers_form_updateUpdate form fields or settings
Search (2 tools)
ToolDescription
layers_item_resolveResolve any Layers item (task/page/project) from a URL or ID
layers_searchFull-text search across tasks, pages, and projects
Navigation (1 tool)
ToolDescription
layers_navigateWalk the hierarchy one level at a time: what is inside this thing?
Users (2 tools)
ToolDescription
layers_user_meGet the currently authenticated user
layers_user_resolveResolve a user by name, email, or ID
Membership (5 tools)
ToolDescription
layers_invitation_acceptAccept a workspace invitation
layers_invitation_deleteRevoke a pending invitation
layers_workspace_inviteInvite a person to a workspace by email, with a role
layers_workspace_kickRemove a member from a workspace
layers_workspace_usage_summarySeats, storage and plan limits for a workspace
Bulk operations (5 tools)
ToolDescription
layers_comment_bulk_createPost comments on many tasks in one call (rate-limited once for the batch)
layers_comments_bulk_listRead comments across many tasks in one call
layers_form_bulk_deleteDelete several forms in one call
layers_task_bulk_createCreate many tasks in one call
layers_task_bulk_updateUpdate many tasks in one call
Context (1 tool)
ToolDescription
layers_context_acknowledgeDiagnostic: reflect back the context the server parsed from _meta
Tool slices (1 tool)
ToolDescription
layers_toolsetsList the tool slices this server can expose, and their token cost

Smart Features

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:

  • Status → first status in project (usually "Backlog")
  • Priority → default priority (usually "Normal")
  • Sprint → latest sprint, or auto-creates "Task List"
  • Task type → "Task"

Usage Examples

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?"


Troubleshooting

Server not appearing in client
  1. Verify JSON syntax: cat your-config.json | python3 -m json.tool
  2. Check Node.js: node --version (need 18+)
  3. Fully restart the client — Cmd+Q (macOS) / Alt+F4 (Windows), not just close the window
  4. nvm users: Use the full path to npx (see Claude Desktop section above)
401 Unauthorized on tool calls

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.

Token expired or invalid

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.

Everything reads fine, but nothing can be changed

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.

OAuth popup appears (mcp-remote)

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
Timeout on startup

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"
      }
    }
  }
}
Behind a corporate VPN

Add your CA certificate:

"env": {
  "NODE_EXTRA_CA_CERTS": "/path/to/corporate-ca.crt"
}
Enable debug logging (mcp-remote)
npx mcp-remote https://YOUR_DOMAIN/mcp --debug

Logs are written to ~/.mcp-auth/.


How It Works

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)


Build from Source

./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 Hub

docker pull sinups/layers-mcp-server:latest

Available for linux/amd64 and linux/arm64.


Tag summary

Content type

Image

Digest

sha256:8606c3916

Size

139.8 MB

Last updated

6 days ago

docker pull sinups/layers-mcp-server