Semaphor
MCP

Connect with a Token

Connect automation, an agent in your product, or your own code to Semaphor with an MCP project token.

Automation (CI jobs, schedulers, scripts) and agents inside your product can't open a browser to log in. They connect with an MCP project token: a short-lived token your backend mints with your project secret, sent on every request as Authorization: Bearer <token>.

Your backendmints a token withthe project secretAgentsends it as abearer tokenSemaphor MCPpins the sessionto one projectYour datathat user'spolicies applied

AutomationorgUserId

The token acts as an organization user. Use it for CI jobs, schedulers and scripts, with write scope if the job creates dashboards.

An end user in your productendUserId

The token acts as one of your end users, with the same security policies as their embedded dashboards. It's always read only.

Mint a token

Call POST https://semaphor.cloud/api/v1/token from your server, the same call you use for embedded dashboards, and add the mcp option.

A nightly job that may create dashboards
curl -X POST https://semaphor.cloud/api/v1/token \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "p_...",
    "projectSecret": "<project-secret>",
    "orgUserId": "<org-user-id>",
    "mcp": { "scopes": ["mcp:read", "mcp:write"] },
    "tokenExpiry": 1800
  }'
A read-only agent for one end user
curl -X POST https://semaphor.cloud/api/v1/token \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "p_...",
    "projectSecret": "<project-secret>",
    "tenantId": "tenant_123",
    "endUserEmail": "maria@acme.com",
    "mcp": { "scopes": ["mcp:read"], "sql": false },
    "semanticDomainAccess": { "mode": "include", "domains": ["sales"] },
    "tokenExpiry": 900
  }'

This token also turns off SQL and limits the agent to one semantic domain.

The response is { "accessToken": "..." }. Keep it out of source control and logs; the examples below read it from SEMAPHOR_MCP_TOKEN.

Keep the project secret on your server

Never expose your projectSecret in client-side code. Mint tokens on your backend and pass only the short-lived accessToken to the agent.

Point a client at the token

Send the token in the Authorization header to https://semaphor.cloud/api/mcp over Streamable HTTP. If you also use the Semaphor plugin, add the token server next to it under another name, such as semaphor-token.

agent.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const transport = new StreamableHTTPClientTransport(
  new URL('https://semaphor.cloud/api/mcp'),
  {
    requestInit: {
      headers: { Authorization: `Bearer ${process.env.SEMAPHOR_MCP_TOKEN}` },
    },
  }
);

const client = new Client({ name: 'acme-insights-agent', version: '1.0.0' });
await client.connect(transport);

// The tool list follows the token's identity and settings
const { tools } = await client.listTools();

const context = await client.callTool({
  name: 'semaphor_get_analysis_context',
  arguments: {},
});
console.log(context.structuredContent);

Claude Code expands ${SEMAPHOR_MCP_TOKEN} from the environment:

.mcp.json
{
  "mcpServers": {
    "semaphor-token": {
      "type": "http",
      "url": "https://semaphor.cloud/api/mcp",
      "headers": { "Authorization": "Bearer ${SEMAPHOR_MCP_TOKEN}" }
    }
  }
}

Or from the command line:

claude mcp add --transport http semaphor-token https://semaphor.cloud/api/mcp --header "Authorization: Bearer ${SEMAPHOR_MCP_TOKEN}"

Codex reads the token from the named variable:

~/.codex/config.toml
[mcp_servers.semaphor-token]
url = "https://semaphor.cloud/api/mcp"
bearer_token_env_var = "SEMAPHOR_MCP_TOKEN"
~/.cursor/mcp.json
{
  "mcpServers": {
    "semaphor-token": {
      "url": "https://semaphor.cloud/api/mcp",
      "headers": { "Authorization": "Bearer ${env:SEMAPHOR_MCP_TOKEN}" }
    }
  }
}

Restart Cursor, then ask it to call semaphor_get_analysis_context. You should see the token's project.

Without an MCP SDK, send JSON-RPC over HTTP. Include both content types in Accept; the server answers with JSON.

List tools
curl -X POST https://semaphor.cloud/api/mcp \
  -H "Authorization: Bearer $SEMAPHOR_MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
Run a governed query
curl -X POST https://semaphor.cloud/api/mcp \
  -H "Authorization: Bearer $SEMAPHOR_MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "semaphor_analyze",
      "arguments": {
        "domainId": "dom_abc",
        "datasetName": "orders",
        "measures": [{ "datasetName": "orders", "name": "order_count" }],
        "dimensions": [{ "datasetName": "orders", "name": "region" }],
        "limit": 10
      }
    },
    "id": 2
  }'
mcp_query.py
import os
import requests

response = requests.post(
    "https://semaphor.cloud/api/mcp",
    headers={
        "Authorization": f"Bearer {os.environ['SEMAPHOR_MCP_TOKEN']}",
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
    },
    json={
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {"name": "semaphor_list_dashboards", "arguments": {"search": "sales"}},
        "id": 1,
    },
)
print(response.json())

Discover and query

The token fixes the project, so the agent never lists or picks projects. It starts with the analysis context and follows the semantic path.

Example conversation
User: "How many orders did we get last month by region?"

Agent calls: semaphor_get_analysis_context
Agent calls: semaphor_list_datasets, semaphor_get_dataset_schema with datasetName "orders"
Agent calls: semaphor_analyze with:
  measures: [{ "datasetName": "orders", "name": "order_count" }]
  dimensions: [{ "datasetName": "orders", "name": "region" }]
  dateField: { "datasetName": "orders", "name": "order_date" }
  timeWindow: { "kind": "calendar", "unit": "month", "period": "previous" }

Agent: "Last month's orders by region: West 1,247, East 1,103, Central 892, South 786"

Tokens with SQL on can also use semaphor_query_sql_advanced for questions the semantic model can't express. See Discover and Query.

Refresh before it expires

Tokens last from 60 seconds to 24 hours (tokenExpiry, default one hour). An expired token gets 401 with "MCP project token is invalid or expired." Mint a new one on your backend and reconnect.

Request Fields

FieldDefaultDescription
orgUserIdThe organization user the token acts as. Use for automation.
endUserIdThe tenant user the token acts as. Use for an end user in your product.
tenantId + endUserEmailAlternative to endUserId: identify the end user by tenant and email.
mcp.scopes["mcp:read"]["mcp:read"] or ["mcp:read", "mcp:write"]. Write is only for organization users with an Author role or higher.
mcp.sqltruefalse turns off SQL and physical table discovery for this token.
tokenExpiry3600Lifetime in whole seconds, from 60 to 86400.

The usual security fields (cls, rcls, params, semanticDomainAccess) apply exactly as they do for embedded dashboards. See the Token API for every field.

A request is never silently downgraded. If you ask for something the identity can't have, such as write scope for an end user, the mint fails.

How Token Sessions Behave

Pinned to one project

The agent never lists or switches projects, and passing a different projectId returns an error. Mint a token for each project the agent needs.

The token's identity

Queries run with that user's role and permissions, plus the token's security settings. Two tokens can get different numbers for the same question.

Write scope is approval

Minting a token with mcp:write approves that job's writes in advance. There's no per-change human approval in a headless session.

Responses

Results arrive as structuredContent and as JSON text in content[0].text. A tool error is a result with isError: true and a code, not an HTTP error.

Unattended Claude Code runs

In a non-interactive claude -p run, a tool call that would ask for permission is denied unless --allowedTools or an allow rule covers it. List the Semaphor tools the job needs, such as --allowedTools "mcp__semaphor-token__semaphor_get_analysis_context,mcp__semaphor-token__semaphor_analyze". If the job should never write, mint its token without mcp:write.

Mint Errors

The mint returns 400 with { "error": "<code>", "message": "<text>" }:

errorFix
mcp_invalid_scopemcp.scopes must be ["mcp:read"] or ["mcp:read", "mcp:write"], and mcp.sql must be true or false.
mcp_write_not_allowedWrite access needs an organization user (orgUserId) with an Author role or higher. End-user tokens are read only.
mcp_token_lifetime_invalidtokenExpiry must be a whole number of seconds from 60 to 86400. Strings such as "2h" aren't accepted.
mcp_identity_requiredAn MCP token acts as one user. Pass orgUserId, or endUserId with tenantId; a tenantId alone isn't enough.

Other Tokens

The MCP accepts only MCP logins and MCP project tokens. Project tokens minted without the mcp option, dashboard embed tokens and API tokens get 401 with "This token was not minted for the MCP." Add the mcp option to the mint call you already make.

Connection Parameters

ParameterValue
MCP endpointhttps://semaphor.cloud/api/mcp (self-hosted: your Semaphor URL followed by /api/mcp)
TransportStreamable HTTP (JSON responses)
Auth headerAuthorization: Bearer <token>
TokenMCP project token from POST /api/v1/token with the mcp option

On this page