Steerd API

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:

EnvironmentBase URL
Local developmenthttp://localhost:3100
Productionhttps://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:

  1. It registers itself with Steerd, which needs no account and no secret.
  2. It opens Steerd in your browser. Sign in if you are not signed in already.
  3. 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.
  4. 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 401 carries WWW-Authenticate: Bearer resource_metadata="…", scope="…", with error="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/register is open and stores nothing. It takes public clients only (token_endpoint_auth_method: none), the authorization_code grant, and one to five redirect URIs. A redirect URI is https on a DNS name, or http on localhost, 127.0.0.1 or [::1] spelled exactly so, where any port matches the registered one (RFC 8252).
  • Authorization at /oauth/authorize requires PKCE with S256, and its response carries iss (RFC 9207).
  • The token endpoint, POST /api/public/v1/oauth/token, takes grant_type=authorization_code with the code verifier and, optionally, resource set 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> and get_<singular> for organizations, contacts, employees, projects, tasks, activities, assignments, and cvs.
  • Files and attachments: list_files/get_file and list_attachments (by parent entity). Metadata only, no file bytes.
  • Search: a global search that matches across the resource types your key can read.
  • Time entries: list_time_entries, get_time_entry, and get_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_entries narrows to a range with date_from and date_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, and list_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 no trips:write scope to grant. Amounts come back as totals, with totals_sealed saying whether they are final: true once the trip is approved, and those numbers never move again.
  • Create and update: create_<resource> and update_<resource> for organizations, contacts, employees, projects, tasks, and cvs; create_activity and create_assignment (create only); and create_time_entry/update_time_entry. Plus archive_project and restore_project. Write tools require a <resource>:write scope 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 (and restore_project to 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_entry is the exception: give it an external_ref derived from the source, and a repeat returns the stored entry with already_logged: true instead of logging the work twice. See Idempotency.
  • Files and attachments are metadata only. Their bytes are binary and are not returned through MCP. get_file returns 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.

On this page