MCP server
Streamable HTTP MCP endpoint at /api/mcp. JSON-RPC 2.0. Designed so an agent can provision, author, query, and summarize a survey end-to-end without a human in the loop.
Discovery
GET /api/mcp
# returns server info, protocol version, and tool names
POST /api/mcp
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
# returns full tool definitions, input schemas, and _meta.estimatedTokenCostAuth
provisionProject is unauthenticated (anyone can sign up). Every other tool requires a Bearer token. We accept two kinds:
# Service key (sk_...) — for headless server-to-server
Authorization: Bearer sk_...
# OAuth access token (at_...) — for agents that can complete the consent flow
Authorization: Bearer at_...OAuth 2.1 + PKCE + DCR
Agents that can open a browser (Claude Code, Cursor) can register dynamically and get tokens scoped to a single project chosen by the user — no admin pre-provisioning.
1. Discover endpoints:
GET /.well-known/oauth-authorization-server
2. Register the client (RFC 7591):
POST /oauth/register
{ "client_name": "Claude Code", "redirect_uris": ["http://localhost:51763/cb"] }
→ { "client_id": "client_..." }
3. Open the authorize URL in the user's browser:
/oauth/authorize?response_type=code
&client_id=client_...
&redirect_uri=http://localhost:51763/cb
&code_challenge=<sha256(verifier)|base64url>
&code_challenge_method=S256
&state=<random>
4. User picks a project, clicks Allow → redirect lands at the local listener:
http://localhost:51763/cb?code=code_...&state=...
5. Exchange the code:
POST /oauth/token
grant_type=authorization_code
code=code_...
client_id=client_...
redirect_uri=http://localhost:51763/cb
code_verifier=<the verifier you hashed in step 3>
→ { "access_token": "at_...", "token_type": "Bearer",
"expires_in": 2592000, "scope": "project",
"project_id": "proj_..." }
6. Use the access token against /api/mcp:
Authorization: Bearer at_...The service key is returned by provisionProject and rotatable via rotateKey. OAuth access tokens expire after 30 days; the agent re-runs the flow to refresh.
Tools
| Tool | Purpose | Auth | in/out tokens |
|---|---|---|---|
| provisionProject | Bootstrap project + keys + integration snippet | none | 60 / 350 |
| createSurvey | Create a draft survey | bearer | 400 / 80 |
| parseSurvey | Parse loose JSON / markdown / prose into validated Question[] | bearer | 350 / 500 |
| editSurvey | Replace draft contents | bearer | 400 / 60 |
| publishSurvey | Promote draft to published | bearer | 80 / 120 |
| listSurveys | Enumerate surveys in a project | bearer | 50 / 250 |
| queryResponses | Raw paginated responses | bearer | 80 / 1500 |
| summarizeResponses | Token-efficient analytics | bearer | 60 / 600 |
| exportInsights | Headline + bullets + summary | bearer | 60 / 900 |
| getUsage | Current period response count | bearer | 40 / 80 |
| listMembers | List humans on a project | bearer | 50 / 200 |
| inviteMember | Invite a teammate by email (unlimited members) | bearer | 80 / 150 |
| removeMember | Remove a project member | bearer | 60 / 30 |
| subscribeWebhook | Register an outbound webhook | bearer | 80 / 100 |
| rotateKey | Rotate publishable or service key | bearer | 50 / 80 |
End-to-end loop
provisionProject({ name }) → { projectId, publishableKey, serviceKey, integrationSnippet }
createSurvey({ projectId, draft }) → { surveyId, version: 1 }
publishSurvey({ projectId, surveyId }) → { publishedVersion, publicUrl }
... respondents submit ...
summarizeResponses({ projectId, surveyId }) → SurveySummary
exportInsights({ projectId, surveyId }) → { headline, bullets, summary }Webhooks
Subscribe via subscribeWebhook to receive POSTs on response.submitted and survey.published. Signed with HMAC-SHA256 via the x-survey-signature header.
// HMAC verify (Node)
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(req, secret) {
const ts = req.headers["x-survey-timestamp"];
const sig = req.headers["x-survey-signature"];
const expected = createHmac("sha256", secret)
.update(`${ts}.${req.rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}