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 seeapps.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_requiredreason: mcp_access_requires_paid_planupgrade_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_readcapability. - Write tools require
can_writecapability.
- Read tools require
- App scope checks: app-key tools that target
/api/apps/:id/*must match the key'sapp_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
- Start in read-only MCP mode.
- Validate parity and tenant-isolation tests in CI.
- Enable write tools only after passing the full security suite.
- 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.
-
Go to Team → MCP Keys in the LinkMe portal and generate an MCP key (works for both personal and team contexts).
-
Add or update
.cursor/mcp.jsonat the repo root:
{
"mcpServers": {
"linkme": {
"url": "https://li-nk.me/mcp",
"headers": {
"Authorization": "Bearer tk_YOUR_KEY"
}
}
}
}
- 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.
- Build Edge once so the MCP server can import the shared policy module:
npm -w apps/edge run build
-
Ensure Edge is running locally (example port
18080). -
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"
- 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