Skip to content

MCP server

Flowstate is an MCP server. Install it once into Claude.ai or ChatGPT, and your engineers can ask questions about workforce, AI spend, projects, scenarios — answered against your live Flowstate data — from the chat window. Same server, two install paths.

For the install side, see Flowstate in Claude and Flowstate in ChatGPT. This page is the protocol reference.

Endpoints

The server is mounted at /api/mcp on every tenant subdomain.

MethodPathPurpose
GET/.well-known/oauth-authorization-serverOAuth discovery (RFC 8414)
POST/api/mcp/oauth/registerDynamic client registration (RFC 7591)
GET / POST/api/mcp/oauth/authorizeOAuth consent and authorization-code issuance
POST/api/mcp/oauth/tokenToken exchange and refresh
POST/api/mcp/protocolMCP JSON-RPC requests (Streamable HTTP)
GET/api/mcp/protocolSSE for server-initiated messages
DELETE/api/mcp/protocolOptional session cleanup

Base URL example:

https://acme.flowstate.inc/api/mcp

Feature toggle

The entire /api/mcp surface is gated behind the per-tenant mcp_external_access feature toggle. With the toggle off, every endpoint returns:

json
{
  "error": "mcp_disabled",
  "error_description": "MCP external access is not enabled for this organization"
}

with HTTP status 403. Contact Flowstate support to flip the toggle on for your tenant.

OAuth flow

1. Discovery

The MCP client fetches the discovery document:

GET https://{tenant}.flowstate.inc/.well-known/oauth-authorization-server

Response:

json
{
  "issuer": "https://{tenant}.flowstate.inc",
  "authorization_endpoint": "https://{tenant}.flowstate.inc/api/mcp/oauth/authorize",
  "token_endpoint": "https://{tenant}.flowstate.inc/api/mcp/oauth/token",
  "registration_endpoint": "https://{tenant}.flowstate.inc/api/mcp/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post"],
  "scopes_supported": ["openid", "profile"]
}

2. Dynamic client registration

The MCP client registers itself:

POST /api/mcp/oauth/register
Content-Type: application/json

{
  "client_name": "Claude.ai",
  "redirect_uris": ["https://claude.ai/api/mcp/callback"]
}

Response (201 Created):

json
{
  "client_id": "mcp_<32 hex>",
  "client_secret": "<64 hex>",
  "client_name": "Claude.ai",
  "redirect_uris": ["https://claude.ai/api/mcp/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "client_secret_post"
}

3. Authorization

Standard authorization-code with PKCE (S256 only). The user is redirected to the consent page on /api/mcp/oauth/authorize and approves the requested scopes. Auth codes have a 5-minute TTL.

4. Token exchange

POST /api/mcp/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=<auth code>&
client_id=<client id>&
client_secret=<client secret>&
redirect_uri=<registered redirect>&
code_verifier=<PKCE verifier>

Response:

json
{
  "access_token": "<JWT, 1 hour>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "<opaque, 30 days>",
  "scope": "openid profile"
}

Refresh tokens are rotated on every use of the refresh_token grant.

Calling tools

Tool calls go to the protocol endpoint:

POST /api/mcp/protocol
Authorization: Bearer <access token>
Content-Type: application/json

{ "jsonrpc": "2.0", "method": "tools/call", "id": 1, "params": { ... } }

The transport is Streamable HTTP (per the MCP spec). A fresh server context is created per request, scoped to the calling user.

Rate limiting

100 requests per minute per user. Implemented as a sliding-window counter. If the limiter's backing store is unreachable the limiter fails open (the request is allowed).

Every response carries:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: <0–100>

Exceeding the limit returns:

json
{
  "jsonrpc": "2.0",
  "error": { "code": -32000, "message": "Rate limit exceeded. Try again shortly." },
  "id": null
}

with HTTP status 429.

Tool catalogue

Read tools

No scenario required.

ToolPurpose
get_organization_contextOrg overview — call this first
search_employeesSearch by name, email, geography or skill
get_employee_detailsFull details including allocations and salary
rank_employeesBy salary or bonus — financial permission required
search_teamsTeam search by name
get_team_detailsMembers, vacancies, contractors, projects
search_projectsBy name, delivery status, owner
get_project_detailsAllocations and financial data
find_projects_with_issuesCompleted-with-allocations or no-allocations
search_contractorsBy name or team
search_vacanciesBy role, team, status
get_geographiesLocations and IDs
list_job_rolesStandardised role titles
query_analyticsSlice-and-dice across teams, projects, employees, cost centres, work types
get_ai_usage_summaryAI spend by system
get_ai_spend_by_teamAI spend by team with adoption rate
get_ai_spend_by_personPer-user AI spend
list_scenariosExisting scenario plans
get_project_effortActual effort breakdown for a project
get_team_effortActual effort breakdown for a team
get_unattributed_effortEffort not linked to any Flowstate project
search_initiativesSearch the finance anchor by name; filter by status, financeMode, or portfolio lens
get_initiative_detailsAn Initiative's finance fields + rollups (effort actuals, forecast, variance), child projects, and the derived capitalisable / isInert signal
list_portfolioThe Objective → Initiative → Project tree with a variance figure per Initiative
list_objectivesObjectives (the strategy layer) with Key-Result progress and linked-initiative counts

Write tools

Every write tool mutates a specific scenario, never live data. The user must call create_scenario first; subsequent write calls take a planId argument.

ToolPurpose
create_scenarioNew scenario; required before any write
add_team, update_team, delete_teamTeam CRUD
add_employee, update_employee, move_employee, terminate_employeesEmployee CRUD + lifecycle
add_vacancy, update_vacancy, delete_vacancyVacancy CRUD
add_contractor, update_contractor, delete_contractorContractor CRUD
add_project, update_project, delete_projectProject CRUD
allocate_employee_to_projectAllocate one engineer
allocate_team_to_projectAllocate a team
update_allocationChange FTE, dates, or end an allocation
add_ai_agent, update_ai_agent, delete_ai_agentAI agent (seat-cost) CRUD
update_budgetSet a budget amount on an effective date

Initiative finance tools

These manage the Initiative finance anchor. Unlike the scenario write tools above, they write LIVE data directly — the Initiative is a live finance/governance object (like a cost centre or a budget), not a scenario edit, so there is no planId and no merge step. Money fields require financial permissions.

ToolPurpose
add_initiativeCreate a human-authored Initiative; optionally set financeMode and link an objective / portfolio lens
update_initiativePatch finance fields (financeMode, cost centres, lens, objective, budgetedCost/budgetCurrency, marginPct, committedFte, AI budget) — partial update
adopt_project_to_initiativeAdopt a project into an Initiative as its finance parent (≤1 parent; rejects with PROJECT_ALREADY_ADOPTED)
detach_project_from_initiativeDetach a project, restoring its own cost centre from the dormant stash
set_initiative_commitmentPlan a commitment (committed FTE + optional AI budget) of an EMPLOYEE / TEAM / CONTRACTOR onto an Initiative over a window
create_capex_claimCreate a CapEx/R&D claim — Initiative-anchored (auto-pulls child projects; Initiative must be CAPEX_RD_CLAIM) or ad-hoc over projectIds; the one-claim-per-project-per-period guard returns PROJECT_ALREADY_CLAIMED

Live writes, not scenarios

The Initiative finance tools mutate live data and inherit the caller's RBAC: add_initiative needs ROADMAP_INITIATIVES_CREATE, update_initiative / adopt / detach / commitment need ROADMAP_INITIATIVES_UPDATE, and create_capex_claim needs FINANCIALS_VIEW_SUMMARY. The GraphQL contract these map onto is documented at API → Initiatives.

Permissions

Tool calls inherit the OAuth user's Flowstate permissions. If the user can't read salaries in the UI, rank_employees returns a permission error from MCP. There's no privilege escalation through the connector.

Audit and SIEM

Every protocol request emits a SIEM event with:

  • The RPC method
  • The OAuth client name
  • The user, org and tenant identifiers
  • Request ID, IP, user agent

Rate-limit hits and request errors emit higher-severity events. If your org has SIEM integration wired up, MCP traffic flows there alongside the rest of the platform.

Scenarios via MCP

Write operations require an explicit planId. The model is:

  1. Call create_scenario({ title, type }) → returns a scenario id.
  2. Pass that id as planId on every subsequent write call.
  3. Once happy with the scenario, the user submits it for approval through the Workforce → Scenarios → Inbox UI. There's no MCP tool for approval — that's deliberately a human gate.

Read tools work against live data by default. To read from a specific scenario, pass planId to query_analytics (it accepts an optional scenario ID for what-if reads).

Disabling

To disable MCP for the whole org, ask Flowstate support to flip mcp_external_access off. The connector immediately starts returning 403 mcp_disabled. Existing tokens are not revoked but become unusable.

To revoke a single user's grant, revoke the OAuth client at Settings → API Keys.

See also

Flowstate Documentation