MCP server
Connect an MCP client and query your Steerd data in natural language.
The Public API also speaks the Model Context Protocol (MCP). Point an MCP-capable
client (Claude Desktop, IDE assistants, and others) at the endpoint and it can read and
update your Steerd data through tools. A client that supports MCP authorization signs you
in through your browser; any client can use a strd_ API key instead, the same key as REST
and GraphQL. Every tool is scoped to one team and to what you may do in it.
Endpoint
The endpoint is POST /api/public/v1/mcp over streamable HTTP, on the app origin for your
environment:
| Environment | Base URL |
|---|---|
| Local development | http://localhost:3100 |
| Production | https://app.steerd.io |
So a local client connects to http://localhost:3100/api/public/v1/mcp and production is
https://app.steerd.io/api/public/v1/mcp. A connection or a key works only against the
environment it was made in. The examples below use production; for local development swap
in http://localhost:3100.
Connect with OAuth
Give the client the endpoint URL and nothing else:
{
"mcpServers": {
"steerd": {
"url": "https://app.steerd.io/api/public/v1/mcp"
}
}
}A client that only speaks stdio can bridge to the remote server with mcp-remote, which
signs you in the same way:
{
"mcpServers": {
"steerd": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://app.steerd.io/api/public/v1/mcp"]
}
}
}Steerd answers the first request with 401 and a challenge that tells the client where to
sign you in. A client that supports MCP authorization takes it from there:
- It registers itself with Steerd, which needs no account and no secret.
- It opens Steerd in your browser. Sign in if you are not signed in already.
- Steerd shows which app is asking, where it sends you back, what it may do, and how long the connection lasts. Pick the team to connect, then choose Allow.
- Your browser returns to the app, and the app finishes connecting.
What a connection may do is fixed: read and log time entries, read projects, and read
clients. Its scopes are time_entries:read, time_entries:write, projects:read and
organizations:read, so the time, project and client tools work and any other tool answers
forbidden.
- It acts as you, in the team you picked. It sees and does what you may do there, and it stops working when you leave the team.
- It lasts 30 days. After that, the client asks you to sign in again.
- It works only for MCP. Its token starts with
strdo_and is refused on REST and GraphQL. - Connecting the same app again replaces the earlier connection in that team.
- You can disconnect it at any time under Settings → Integrations → Connected apps, which lists each connection with the app's name and where it sends you back.
Connecting needs API access, a paid feature that is part of the 30-day trial, and a role that may connect apps, which a time tracker's role may not. A connection counts toward your plan's API key limit. The consent page lists all your teams and says why one cannot be picked.
An app connected this way is marked Unverified app: anyone can register one, and the name it shows is its own claim. Allow a connection only when you just started it yourself.
Connect with an API key
Any client can send an API key instead, as a bearer token:
Authorization: Bearer strd_<key>The key resolves your team and the tools you may call. Create one in the Steerd app under Settings → API keys, granting only the scopes the integration needs (see authentication). A key can carry scopes that a connection cannot, for the tools beyond time, projects and clients, and it lasts until it is revoked. A key handed to an assistant can read everything its scopes allow, so prefer a least-privilege key.
{
"mcpServers": {
"steerd": {
"url": "https://app.steerd.io/api/public/v1/mcp",
"headers": { "Authorization": "Bearer strd_YOUR_KEY" }
}
}
}With mcp-remote, pass the key as a header:
{
"mcpServers": {
"steerd": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://app.steerd.io/api/public/v1/mcp",
"--header",
"Authorization: Bearer strd_YOUR_KEY"
]
}
}
}Clients that install servers from the official MCP Registry
find Steerd there as io.steerd/steerd, with the production endpoint filled in. Its API key
header is optional: leave it empty to connect with OAuth.
For client developers
Steerd implements the MCP authorization specification:
- The
401carriesWWW-Authenticate: Bearer resource_metadata="…", scope="…", witherror="invalid_token"added when the request sent a credential that was refused. - Protected resource metadata (RFC 9728) is served
at
/.well-known/oauth-protected-resource/api/public/v1/mcp, and authorization server metadata (RFC 8414) at/.well-known/oauth-authorization-server, both on the app origin. - Registration (RFC 7591) at
POST /api/public/v1/oauth/registeris open and stores nothing. It takes public clients only (token_endpoint_auth_method: none), theauthorization_codegrant, and one to five redirect URIs. A redirect URI ishttpson a DNS name, orhttponlocalhost,127.0.0.1or[::1]spelled exactly so, where any port matches the registered one (RFC 8252). - Authorization at
/oauth/authorizerequires PKCE withS256, and its response carriesiss(RFC 9207). - The token endpoint,
POST /api/public/v1/oauth/token, takesgrant_type=authorization_codewith the code verifier and, optionally,resourceset to the endpoint URL. Errors follow RFC 6749. There is no refresh token: after 30 days the client authorizes again, on the registration it already has.
Tools
Tools mirror the REST and GraphQL resources and return the same public projections (no
team_id, and so on). Names follow a verb_object convention.
- List and get:
list_<resource>andget_<singular>for organizations, contacts, employees, projects, tasks, activities, assignments, and cvs. - Files and attachments:
list_files/get_fileandlist_attachments(by parent entity). Metadata only, no file bytes. - Search: a global
searchthat matches across the resource types your key can read. - Time entries:
list_time_entries,get_time_entry, andget_time_summary. These honor your role: a time tool reflects exactly what its key's creator may see and do in the app, and refuses when that access is revoked.list_time_entriesnarrows to a range withdate_fromanddate_to, both included, so the same date in both reads one day and an assistant can see what that day already holds before it logs more. The other time-entry filters are on REST and GraphQL. - Trips:
list_trips,get_trip, andlist_trip_expenses. Read-only, and role-aware like the time tools: a travel manager's key sees the team's trips, anyone else's sees only their own. A trip is created and sealed in the app, so there is no write tool and notrips:writescope to grant. Amounts come back astotals, withtotals_sealedsaying whether they are final:trueonce the trip is approved, and those numbers never move again. - Create and update:
create_<resource>andupdate_<resource>for organizations, contacts, employees, projects, tasks, and cvs;create_activityandcreate_assignment(create only); andcreate_time_entry/update_time_entry. Plusarchive_projectandrestore_project. Write tools require a<resource>:writescope and validate exactly as REST does.
Each tool mirrors the same team-scoping, validation, and public projections as the equivalent REST request.
List results are paged
List tools return at most 25 items by default and 50 at most. To page, pass the
nextCursor from a result back as the cursor argument, the same cursor connection REST
and GraphQL use.
What is not a tool
- No delete. Destructive deletes are never exposed to an assistant. To set a project
aside, use
archive_project(andrestore_projectto bring it back); for permanent deletion, use the REST or GraphQL API, or the app. - Writes are at-least-once, except a time entry that names its work. A retried
create_call may create a duplicate; there is no idempotency key on MCP writes.create_time_entryis the exception: give it anexternal_refderived from the source, and a repeat returns the stored entry withalready_logged: trueinstead of logging the work twice. See Idempotency. - Files and attachments are metadata only. Their bytes are binary and are not
returned through MCP.
get_filereturns the record; fetch the bytes from the REST download endpoint with the same key. - No uploads. File uploads are multipart and go through REST.
- No trip writes. Trips are drafted, submitted, and approved in the app, where the approval seals the numbers. Nothing an assistant does can create or alter one.
A note on trust
An assistant reads your Steerd data through these tools, and some of that data is free text that other people wrote (a project note, an incoming request, a contact's details). Treat tool results as data, not instructions. Grant write scopes only when the integration needs them, and keep the key least-privilege, so a surprising or manipulated result can only ever do what the key's scopes allow.
Errors
Tool errors carry a stable machine code (forbidden, not_found, bad_request,
limit_reached, internal) and a safe message. A limit_reached error includes which
plan limit was hit, so an assistant can suggest an upgrade rather than retry. See
errors for the shared code list.