MCP Security & Access Model
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"
}
}
}
}
3. 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
2. Ensure Edge is running locally (example port 18080).
3. 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"
4. 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
- API Keys & Authentication
- REST Endpoints & OpenAPI
- Developer Overview