Home Developer Reference MCP Security & Access Model

MCP Security & Access Model

Last updated on Apr 09, 2026

MCP security & access model

LinkMe treats MCP as a strict adapter over the Edge REST API, not as a privileged backend.

Security invariant

For any caller principal (session, app key, or MCP key):

Allowed(MCP) ⊆ Allowed(REST)

If a request would be forbidden over REST, it must also be forbidden via MCP.

What MCP is allowed to do

  • Call only explicitly allowlisted public /api/* endpoints.
  • Use the same authentication context as the caller:
    • session/cookie context for portal users
    • app API key context for app-scoped automation
    • MCP key context for personal or team-scoped access
  • Enforce app scope and capability checks before forwarding requests.

What MCP is not allowed to do

  • Access /internal/* endpoints.
  • Bypass auth, ownership, or tenant boundaries.
  • Use elevated service credentials to impersonate broader access.
  • Call undocumented/unallowlisted tools.

Authentication

The MCP endpoint authenticates callers via HTTP headers on every request. Three modes are supported:

MCP API key (recommended)

MCP keys can be personal (no team) or team-scoped. Generate one in the portal under Team → MCP Keys. Available on the Indie plan and above.

Header Value
Authorization Bearer tk_...

The key acts as the user who created it. Team-scoped keys grant access to all apps within that team; personal keys grant access to the user's own apps. can_read / can_write capabilities control which tools are available. The key prefix tk_ distinguishes MCP keys from app keys (ak_).

Tool visibility differs by key type:

  • Team-scoped keys see team tools (teams.get, apps.listByTeam) with the team ID auto-filled from the key. They do not see apps.list.
  • Personal keys see apps.list (returns all apps the user owns or has access to). They do not see team-scoped tools.

App API key

Scoped to a single app. Pass X-App-Id and X-Api-Key headers. Generate in App → Developer → API Keys.

Session cookie

Portal users already signed in can authenticate via their session cookie. Primarily useful for browser-based MCP clients.

Plan enforcement

  • MCP access is enforced at call time and follows plan entitlements.
  • Free plan users may still attempt tool calls, but the server rejects them with:
    • error: paid_plan_required
    • reason: mcp_access_requires_paid_plan
    • upgrade_url: https://li-nk.me/portal/account
  • This keeps tool discovery and request flow predictable while ensuring execution stays aligned with billing entitlements.

Available tools

Tool Method Path Access Auth modes
health.get GET /api/health read session, app_key, team_key
teams.get GET /api/teams/:id read session, team_key
apps.list GET /api/apps read session, team_key
apps.listByTeam GET /api/teams/:id/apps read session, team_key
apps.get GET /api/apps/:id read session, app_key, team_key
links.listByApp GET /api/apps/:id/links read session, app_key, team_key
links.getInsights GET /api/links/:id/insights read session, app_key, team_key
links.getDetails GET /api/link-details read session, app_key, team_key
links.create POST /api/apps/:id/links write session, app_key, team_key
links.update PATCH /api/links/:id write session, app_key, team_key

Write tools are disabled by default and require MCP_WRITE_ENABLED=1 on the server.

Access controls

  • Tool allowlist: each MCP tool is pinned to one REST method and path template.
  • Auth mode restrictions: tools define which auth modes (session, app_key, team_key) are accepted.
  • Capability checks:
    • Read tools require can_read capability.
    • Write tools require can_write capability.
  • App scope checks: app-key tools that target /api/apps/:id/* must match the key's app_id. MCP keys skip this check — app ownership is enforced by the REST layer via the synthesized session.
  • Team ID auto-fill: team-scoped MCP keys automatically inject the team ID into team-scoped tool paths (/api/teams/:id/*), so the caller does not need to provide it.
  • Tool filtering: personal MCP keys only see personal tools; team-scoped keys only see team tools. This prevents the LLM from calling tools it cannot authenticate against.
  • Write gate: write tools are disabled by default and require MCP_WRITE_ENABLED=1.

Validation and parity testing

LinkMe includes security tests that simulate natural-language asks and protocol calls, then verifies policy and REST parity:

  • question-to-tool security scenarios (deny escalation attempts)
  • MCP protocol tests (initialize, tools/list, tools/call)
  • MCP-vs-REST status parity for the same principal and target resources
  • personal key and team key authorization scenarios

These checks run locally in the E2E harness and are intended to be required in CI.

Recommended rollout

  1. Start in read-only MCP mode.
  2. Validate parity and tenant-isolation tests in CI.
  3. Enable write tools only after passing the full security suite.
  4. Keep audit logging and alerting on denied/suspicious calls.

Connect from Cursor (remote)

The MCP server is deployed at https://li-nk.me/mcp using Streamable HTTP transport. No local process is needed.

  1. Go to Team → MCP Keys in the LinkMe portal and generate an MCP key (works for both personal and team contexts).

  2. Add or update .cursor/mcp.json at the repo root:

{
  "mcpServers": {
    "linkme": {
      "url": "https://li-nk.me/mcp",
      "headers": {
        "Authorization": "Bearer tk_YOUR_KEY"
      }
    }
  }
}
  1. Restart Cursor (or reload the window) so the MCP server is picked up.

:::caution Do not commit real API keys to version control. Either use placeholder values in the checked-in file and override locally, or add .cursor/mcp.json to .gitignore. :::

Connect from Codex (local)

Use this when running MCP against your local Edge harness.

  1. Build Edge once so the MCP server can import the shared policy module:
npm -w apps/edge run build
  1. Ensure Edge is running locally (example port 18080).

  2. Add an MCP server entry in ~/.codex/config.toml:

[mcp_servers."linkme-local"]
command = "node"
args = ["/Users/tomasradvansky/R-DEV/link-me/apps/mcp/server.mjs"]

[mcp_servers."linkme-local".env]
EDGE_BASE_URL = "http://127.0.0.1:18080"
MCP_WRITE_ENABLED = "0"
  1. Restart Codex so MCP servers reload.

Optional: enable write tools

Only after parity/security tests are green:

[mcp_servers."linkme-local".env]
EDGE_BASE_URL = "http://127.0.0.1:18080"
MCP_WRITE_ENABLED = "1"

Local smoke test (without Codex)

You can validate protocol wiring directly from terminal:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"health.get","arguments":{"auth":{"type":"session","userId":"user_demo","cookie":"YOUR_SESSION_COOKIE"}}}}' \
| EDGE_BASE_URL="http://127.0.0.1:18080" MCP_WRITE_ENABLED="0" node apps/mcp/server.mjs

Related docs