MCP server
Flowstate’s MCP server lets Claude, ChatGPT or another AI assistant read your Flowstate data and make changes in a scenario. You ask a question in the assistant, and it looks up the answer in Flowstate. It acts as you: it sees only what you can see in Flowstate, and changes only what you can change.
You need MCP access switched on for your organisation. Ask your Flowstate contact.
Choose your assistant
| You use | Follow |
|---|---|
| Claude | Connect Flowstate to Claude |
| ChatGPT | Connect Flowstate to ChatGPT |
| Another assistant that supports MCP | Connect any other assistant, below |
Connect any other assistant
- In Flowstate, select Ask Eddy in the top bar, then New chat (the +). Under Connect from Claude or ChatGPT, select Copy MCP URL. The address is your Flowstate address followed by
/api/mcp/protocol, for examplehttps://acme.flowstate.inc/api/mcp/protocol. - In your assistant, add a remote MCP server and paste that address.
- When your assistant opens the Flowstate page, sign in with your Flowstate account and select Approve.
What you can ask
Try questions like these:
- “Who joins the Platform team next month, and what will they cost?”
- “What is the Payments team working on, and how much effort went into each project last quarter?”
- “Which teams spent the most on AI this month?”
- “How far has the forecast moved from the budget we locked in January?”
- “Which projects are completed but still have people allocated?”
- “Who hasn’t submitted their effort report for the last four weeks?”
- “How much capitalisable spend haven’t we claimed yet this year?”
- “Show me the functional structure under Engineering, with open seats.”
What it can change
In a scenario. Adding, moving or removing people, teams, vacancies, contractors, projects, allocations, budgets and AI agents happens in a scenario, never in live data. You see the changes in Flowstate, and they reach live data only when someone submits, approves and merges the scenario there. See Live data and scenarios.
In live data, straight away. Some changes apply as soon as the assistant makes them, exactly as if you’d made them in Flowstate:
- the budget workflow: budget requests and proposals
- initiatives, objectives and key results
- CapEx claims and capitalisation records
- functional groups, and who manages them
- which project your own AI sessions count towards
Not at all. It can’t approve or merge a scenario, lock a budget, change settings (apart from who may see functional group cost), or do anything your Flowstate role doesn’t allow.
Tool catalogue
These are the 107 tools the assistant can call. Every tool runs with the permissions of the person who connected: if you can’t see salaries in Flowstate, a tool that needs them refuses or leaves them out. Where a tool needs a permission most people don’t have, it’s noted.
Start here
| Tool | What it answers |
|---|---|
get_organization_context | Your organisation’s reporting currency and how many teams, people and projects it has |
get_geographies | Your locations and their IDs |
list_job_roles | Your job roles; takes an optional query |
list_custom_attributes | Your custom fields, to filter the search_* tools with customAttributeFilters; takes an optional entityType |
list_scenarios | Existing scenarios |
People and teams
| Tool | What it answers |
|---|---|
search_employees | Find people by name, email, location, skill or custom field |
get_employee_details | One person, with their team and project allocations and salary |
rank_employees | The highest or lowest paid people, by SALARY or BONUS. Needs View Detailed Financials |
search_teams | Find a team by name or custom field and get its ID. Archived teams are left out unless archived is archived or all; an empty query matches every team |
get_team_details | One team now: its people, open roles, contractors, parent and child teams, the projects it’s allocated to, and whether it’s archived |
search_contractors | Find contractors by name, team or custom field; activeOnly defaults to true |
search_vacancies | Find vacancies by role, team, status or custom field; status defaults to open |
Projects, initiatives and objectives
| Tool | What it answers |
|---|---|
search_projects | Find projects by name, deliveryStatus, owner or custom field. pmsLinked: false finds projects not linked to Jira, Azure DevOps or Linear; sourceSystem filters by how the project was created. Returns the total number of matches as well as the page |
get_project_details | One project’s allocations, costs, how it was created and which PMS projects it’s linked to |
find_projects_with_issues | Projects that are completed but still have allocations, or have no allocations at all |
search_initiatives | Find initiatives by name, status, financeMode or portfolio lens |
get_initiative_details | One initiative’s finance fields, effort actuals, forecast and variance, its child projects, and whether it can be capitalised |
list_portfolio | The objective → initiative → project tree, with a variance figure for each initiative |
list_objectives | Objectives with key result progress and how many initiatives each has |
get_project_value_summary | Whether a project returned what it promised: cost to build against forecast, AI share, and each value driver’s before, target and after. Pass projectId or projectName, or neither for the top projects |
Effort
| Tool | What it answers |
|---|---|
get_project_effort | The effort actually reported on a project, by person, with cost if you can see it |
get_team_effort | What a team reported time on, by project, with cost if you can see it. Values are person-days over the period |
get_unattributed_effort | Effort not linked to any Flowstate project |
get_effort_gaps | People active in the period with no tracked effort, or with tracked effort but no report filed |
get_uncosted_effort | People with effort but no salary or rate, so their effort is costed at nothing |
get_submission_leaderboard | On-time and late effort report submissions, per person |
Effort tools default to the last three months; pass startDate and endDate to change the period.
Analytics and the forecast
| Tool | What it answers |
|---|---|
query_analytics | Any “X by Y” question: cost, FTE and headcount (actual or forecast), AI spend and usage, pull requests, and effort submissions, broken down by team, project, person, location, cost centre, provider, model, repository and more. Pass planId to query a scenario. Cost needs View Financial Summary |
query_analytics takes metrics, dimensions and dataSource (ACTUAL, FORECAST or SUBMISSION), plus optional startDate, endDate, timeGranularity (MONTH, QUARTER or YEAR), filters, costSection, and sort, order and limit for top-N questions (limit is at most 25). The tool’s own description lists every metric and dimension. For AI spend examples, see AI usage.
Budgets
| Tool | What it answers |
|---|---|
list_budget_requests | Budget requests, by fiscalYear and status |
get_budget_request | One budget request with its proposals and their statuses |
get_budget_proposal | One proposal’s status, assignee and approver |
get_budget_envelope | A team’s budget envelope for a fiscal year, and where it differs |
list_forecast_budget_snapshots | The locked budgets the forecast is compared against. Needs View Financial Summary |
get_forecast_budget_drift_summary | How far the live forecast has moved from a locked budget; takes a snapshotId. Needs View Financial Summary |
CapEx and R&D
| Tool | What it answers |
|---|---|
get_capex_project_breakdown | CapEx cost per project, by resource type. Needs View Financial Summary |
get_capex_initiative_breakdown | Capitalisation cost rolled up by initiative; filter by capitalisationStatuses. Needs View Financial Summary |
get_capex_declared_vs_claimed | Declared against claimed effort cost, showing capitalisable spend you haven’t claimed. Needs View Financial Summary |
list_capex_claims | CapEx claims with their initiative, project count and status. Needs View R&D Tax Relief |
list_rd_claims | R&D tax claims with stage, project, fiscal year and amounts. Needs View R&D Tax Relief |
The three breakdowns take submittedOnly: true counts submitted effort only, false includes effort still in draft. It defaults to false, except on get_capex_declared_vs_claimed, where it defaults to true.
Code delivery
Need View Effort Tracking. Cost per person needs View Financial Summary.
| Tool | What it answers |
|---|---|
get_code_delivery_summary | Pull request throughput for a period with cost and effort; filter by teamIds, repoNames or initiativeIds |
get_code_delivery_trends | Monthly code delivery and spend per initiative, with the same filters |
list_pull_requests | Pull requests with cost, effort and their project and initiative. unlinked: true finds ones linked to neither; page and limit page through them |
AI and insights
| Tool | What it answers |
|---|---|
get_ai_adoption_summary | People using AI in the last 30 days, weekly active users, tools in use, idle licensed seats and what they cost, and the teams adopting most and least |
get_ai_security_metrics | Shadow AI use, data-loss signals and customer-data exfiltration events; groupBy is provider, subject or team |
get_hybrid_workforce_summary | Employees, contractors and AI agents: headcount, monthly cost, share and change against a year ago, and AI spend against the monthly cap. Needs View Financial Summary |
get_business_metrics_summary | Every active business metric with its function, volume, AI share, cost per unit and owner. Needs View Business Metrics; cost needs View Financial Summary |
For AI spend and usage by team, person, provider or model, use query_analytics.
Functional groups
| Tool | What it answers |
|---|---|
list_functional_groups | The whole functional structure on one day: headcount, filled and open seats, FTE shortfall, arrivals and leavers in the next 30 days, managers and monthly cost |
get_functional_group | One group in full: seats and who fills them, managers, the next twelve months’ schedule and headcount, and its history |
Both take asOf (a day, YYYY-MM-DD, default today) and an optional planId to read a scenario.
Change a scenario
Every tool here needs Edit Scenario Plans and a planId for a scenario that’s still a draft. Create one first with create_scenario.
| Tool | What it does |
|---|---|
create_scenario | Create a scenario and return its ID; takes title and an optional type (HEADCOUNT, BUDGET or RESTRUCTURE). Needs Use Scenario Plans |
add_team, update_team, delete_team | Add, change or remove a team |
add_employee, update_employee, move_employee | Add a person, change their salary, role, location or manager, or move them to another team |
terminate_employees | End one or more people’s employment on a date; employeeIds is a comma-separated list |
add_vacancy, update_vacancy, delete_vacancy | Add planned hires (count adds several at once), delay or change one, or remove it. targetEndDate makes a role fixed-term |
add_contractor, update_contractor, delete_contractor | Add, change or remove a contractor, with their rate and rateType |
add_project, update_project, delete_project | Add, change or remove a project |
allocate_employee_to_project, allocate_contractor_to_project, allocate_vacancy_to_project, allocate_team_to_project | Put a person, contractor, vacancy or team on a project at an FTE, for optional dates |
update_allocation | Change an allocation’s FTE or end date |
remove_allocation | Remove an allocation |
add_ai_agent, update_ai_agent, delete_ai_agent | Add, change or remove an AI agent. Each runs a model from the catalogue and needs a human owner (ownerEmployeeId or ownerContractorId) |
update_budget | Set a budget amount from an effectiveDate |
Change functional groups
These change live data unless you pass a planId. Each follows the functional group’s own access: Edit all functional groups, or being a manager of the group or a group above it. The tools refuse with FEATURE_DISABLED if your organisation doesn’t have functional groups. See Map your functional organisation.
| Tool | What it does |
|---|---|
add_functional_group | Create a group from a start date. Without parentFunctionalGroupId it’s a top-level group, which needs Edit all functional groups |
update_functional_group | Replace a group’s details. Send everything you want to keep: optional fields you leave out are cleared, apart from the type |
move_functional_group | File a group under another, or at the top, from an effectiveDate |
add_functional_position | Create a seat with the FTE it needs |
update_functional_position | Replace a seat’s details; fields you leave out are cleared |
fill_functional_position | Put exactly one employee, contractor or vacancy in a seat, for a share of it and set dates |
update_functional_assignment | Change someone’s share of a seat or their dates |
move_functional_position | From a day, change who a seat reports to (kind: POSITION), or move a person to another seat in the same group (kind: ASSIGNMENT) |
add_functional_group_manager, remove_functional_group_manager | Appoint or remove a manager. Needs Share functional groups or being a manager of that group. Always changes live access, even with a planId |
set_functional_group_cost_visibility | Who may see what groups cost: HIDDEN, MANAGERS or EVERYONE. The same setting as Settings → Delivery → Functional groups, and needs the same access |
Change budgets
These change live data, and work through the same steps as the budget workflow in Flowstate.
| Tool | What it does |
|---|---|
create_budget_request | Start a budget request for a fiscalYear. Needs Create Budget Requests |
cancel_budget_request | Cancel a budget request so you can start again for that year. Needs Create Budget Requests |
assign_budget_proposal | Give a team without a manager’s proposal to someone. Needs Edit Budget Proposals |
submit_budget_proposal | Submit a proposal for review. Needs Submit Budget Proposals |
approve_budget_proposal | Approve a submitted proposal; a team’s proposal rolls up into its parent team’s. Needs Approve Budget Proposals |
reject_budget_proposal | Send a proposal back to its assignee, with an optional comment. Needs Approve Budget Proposals |
merge_budget_proposal | Doesn’t merge. It explains that you finish a budget by locking the budget request in Flowstate |
add_budget_proposal_comment | Comment on a proposal. Needs Edit Budget Proposals, and only an approver or the assignee can comment |
Change initiatives, objectives and CapEx
These change live data. They take no planId.
| Tool | What it does |
|---|---|
add_initiative | Create an initiative, optionally with its financeMode, objective and portfolio lens. Needs Create Initiatives |
update_initiative | Change only the finance fields you pass; null clears one. Needs Update Initiatives |
delete_initiative | Delete an initiative. Needs Delete Initiatives |
adopt_project_to_initiative | Put a project under an initiative. A project can have only one; otherwise it refuses with PROJECT_ALREADY_ADOPTED. Needs Update Initiatives |
detach_project_from_initiative | Take a project out of its initiative, restoring its own cost centre. Needs Update Initiatives |
add_objective | Create an objective. Needs Create Initiatives |
update_objective, add_key_result, update_key_result, delete_key_result | Change an objective, or add, change or delete its key results (each with a target and INCREASE or DECREASE). Needs Update Initiatives |
delete_objective | Delete an objective and its key results. Needs Delete Initiatives |
create_capex_claim | Create a CapEx or R&D claim for a period: from an initiativeId (its projects come with it), or from chosen projectIds. A project can be in one claim per period; a second refuses with PROJECT_ALREADY_CLAIMED. Needs Create R&D Tax Relief Claims |
delete_capitalisation | Delete a project’s capitalisation record (not a CapEx claim). Needs Delete Financial Configuration |
Your own AI sessions
| Tool | What it does |
|---|---|
mark_my_ai_sessions_as_project | Count your own AI sessions towards a project, so their spend shows against it. Sessions that aren’t yours are skipped. Changes live data |
If something’s not right
“MCP external access is not enabled for this organization” — MCP is switched off for your organisation. Ask your Flowstate contact.
There’s no Connect from Claude or ChatGPT box in Eddy — MCP isn’t switched on for your organisation yet.
“User is inactive or not a member of this organization” — your Flowstate account has been deactivated or moved to another organisation. Ask a Flowstate admin, then connect again.
The assistant says you don’t have permission — your Flowstate role doesn’t allow it. Salaries and costs, for example, need a financial permission. Ask a Flowstate admin.
A scenario change is refused — the scenario isn’t a draft any more, or it belongs to a budget proposal that’s been submitted. Create a new scenario, or reopen the proposal in Flowstate.
“Rate limit exceeded. Try again shortly.” — you’ve made too many requests in a minute. Wait a minute and ask again.
To disconnect, remove the Flowstate connector from your assistant. If MCP is switched off for your organisation, every connection stops working straight away.
For developers
This part is for people building their own MCP client or checking a connection.
Transport and endpoint
The server speaks MCP over Streamable HTTP, without sessions:
https://{tenant}.flowstate.inc/api/mcp/protocol
| Method | Path | Purpose |
|---|---|---|
| GET | /.well-known/oauth-authorization-server | OAuth server metadata (RFC 8414) |
| POST | /api/mcp/oauth/register | Dynamic client registration (RFC 7591) |
| GET / POST | /api/mcp/oauth/authorize | Sign-in and consent; issues the authorisation code |
| POST | /api/mcp/oauth/token | Code exchange and refresh |
| POST | /api/mcp/protocol | MCP JSON-RPC requests |
| GET | /api/mcp/protocol | Server-sent events stream |
| DELETE | /api/mcp/protocol | Session end (accepted; there’s no session to close) |
Every protocol request carries the access token:
POST /api/mcp/protocol
Authorization: Bearer <access token>
Content-Type: application/json
Accept: application/json, text/event-stream
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
Tool results come back as a single text content item holding JSON. A tool refuses in one of two ways: its JSON has an error field, or the result is marked isError: true with the reason as text. A missing permission reads Forbidden: You do not have the required permission: ….
Authentication
MCP uses OAuth 2.0 authorisation code with PKCE. Each person signs in with their own Flowstate account. There’s no API key for MCP: REST API keys from Settings → Users & Access → API Keys don’t work on /api/mcp/protocol, and MCP connections don’t appear on that page.
1. Discover. GET /.well-known/oauth-authorization-server on your Flowstate address:
{
"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. Register. No authentication needed:
POST /api/mcp/oauth/register
Content-Type: application/json
{ "client_name": "My assistant", "redirect_uris": ["https://example.com/callback"] }
client_nameis required, at most 200 characters.redirect_urisholds one to five addresses, eachhttps, orhttponlocalhost,127.0.0.1or[::1].
The 201 response carries client_id and client_secret, with token_endpoint_auth_method: "client_secret_post". Registration is limited to 20 an hour per IP address.
3. Authorise. Send the person to /api/mcp/oauth/authorize with:
response_type=codeclient_idandredirect_uri, which must be one you registeredcode_challengeandcode_challenge_method=S256- an optional
state
If they aren’t signed in to Flowstate, they’re asked to sign in first. When they approve, you get code and state back on the redirect; if they deny, you get error=access_denied. Codes can be used once, within five minutes.
4. Exchange the code.
POST /api/mcp/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=<code>&client_id=<client id>&client_secret=<client secret>&redirect_uri=<redirect uri>&code_verifier=<PKCE verifier>
{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<opaque>"
}
5. Refresh. Send grant_type=refresh_token with refresh_token, client_id and client_secret.
- Access tokens last an hour.
- Refresh tokens last 30 days.
- Each refresh returns a new refresh token, and the old one stops working.
Permissions aren’t stored in the token. Every call checks the person’s current Flowstate role, so a role change applies straight away. A person who’s deactivated, or whose organisation has MCP switched off, is refused on their next call.
Scenarios
Scenario writes take a planId:
- Call
create_scenariofor a new scenario, orlist_scenariosto find an existing one. - Pass its
idasplanIdon each write. - Read it back with
query_analytics, or the functional group reads, using the sameplanId.
A write is refused if the person doesn’t have Edit Scenario Plans, if the scenario belongs to another organisation, if it isn’t a draft, or if it backs a budget proposal that’s been submitted, is in review or has been approved. Merging happens in Flowstate. See Scenarios.
Rate limiting
Protocol requests are limited to 100 a minute per person, in fixed one-minute windows. POST responses carry:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: <0–100>
Over the limit:
HTTP/1.1 429 Too Many Requests
{
"jsonrpc": "2.0",
"error": { "code": -32000, "message": "Rate limit exceeded. Try again shortly." },
"id": null
}
Errors
| Status | When | Body |
|---|---|---|
400 | A missing or invalid OAuth parameter, an unknown client, an unregistered redirect address, or an expired or reused code or refresh token | { "error": "invalid_request" | "invalid_client" | "invalid_grant" | "unsupported_grant_type", "error_description": "…" } |
401 | No bearer token, or it’s invalid or expired | { "error": "Missing or invalid Authorization header" }, { "error": "Invalid token" } or { "error": "Token expired" } |
401 | The person is deactivated or no longer in the organisation | { "error": "User is inactive or not a member of this organization" } |
401 | Wrong client secret at the token endpoint | { "error": "invalid_client" } |
403 | MCP is switched off, on a protocol request | { "error": "MCP external access is not enabled for this organization" } |
403 | MCP is switched off, during sign-in or refresh | { "error": "mcp_disabled", "error_description": "MCP external access is not enabled for this organization" } |
429 | Rate limit reached | JSON-RPC error -32000, as above |
500 | The request failed on our side | JSON-RPC error -32603, "Internal server error" |
When a token expires, refresh it and retry. On 429, wait for the next minute.
Audit
Each protocol request is recorded as an audit event with:
- the MCP method
- the person, organisation and tenant
- the request ID, IP address and user agent
Consent, token issue, refresh and client registration are recorded too. If you’ve connected a SIEM, these events arrive there with the rest of Flowstate’s.
See also
- Connect Flowstate to Claude
- Connect Flowstate to ChatGPT
- AI usage — AI spend questions through
query_analytics - Scenarios — what REST and MCP can each do with scenarios