Our MCP server, api.datacircle.dev/mcp, is how Claude, ChatGPT or Cursor use Datacircle: six tools, from fetching a LinkedIn profile to a download link for the free file. On October 9 the listings doer, the AI worker that gets us listed wherever people look for tools, put it in Manufact's MCP catalog. The catalog reviews a server by itself: it connects, signs in through our OAuth, reads what the server declares about itself and its tools, and scores it out of 100.
At 19:07 UTC it gave us 89. Here is what happened next. Nobody asked Wayne, our founder, for anything.
70 minutes, one ticket
The review's notes were precise: server metadata 7 out of 10, because initialize answered no description and no icon. The listings doer can't change the API. The doer, our lead software engineer and another AI worker, can. Between them, a ticket, the only way our workers ask each other for anything.
| UTC | Who | What |
|---|---|---|
| 19:07 | Listings doer | Manufact's review: 89/100, server metadata 7/10 |
| 19:10 | Listings doer | Files T-0487: the two missing fields, the sentence to use, how to check it on idle |
| 19:11 | Doer | Puts it at the top of its queue |
| 19:13 | Doer | Builds it: 8 lines in api/mcp.py, and a test that asserts both fields |
| 19:48 | Pre-releaser | It's on idle, the second copy of production that becomes live at the next release |
| 19:52 | Doer | Tests it on idle: accepted. Starts the release: 39 commits |
| 19:59 | Doer | Idle becomes live. The switch pauses live for 0.8 s, and no request arrives in that time |
| 20:11 | Doer | Tests it on live: accepted. Tells the listings doer on its ticket |
| 20:17 | Listings doer | Manufact's review again: 92/100, server metadata 10/10 |
The change itself is small. MCP 2025-11-25's serverInfo has a description and icons; ours had neither:
SERVER_INFO = {
"name": "datacircle",
"title": "Datacircle",
"version": SERVER_VERSION,
"description": "Query your favorite B2B data APIs through us. Same request, same price, no markup.",
"icons": [{"src": "https://datacircle.dev/favicon.png", "mimeType": "image/png", "sizes": ["1024x1024"]}],
"websiteUrl": "https://datacircle.dev",
}
The description isn't new copy. It's the line every one of our workers uses to say what we do, from one file of approved sentences, word for word.
The last 8 points
At 20:17 every point we still missed was in tool documentation, 32.4 of 40. Each of the six tools scored 0 on outputs and 75 on annotations, and the review said why, the same two lines for all six:
No output schema is declared. Add one for structured results so clients can interpret returned fields.
Declare idempotentHint based on the tool’s actual behavior.
Three minutes later the listings doer filed T-0489. The doer triaged it at 20:25 and committed it at 20:31: 144 lines changed in api/mcp.py. Four choices in it are worth sharing with anyone who runs an MCP server.
1. The output schema is the API's, not a second copy
Every tool is one of our REST calls underneath, and docs/openapi.json already documents each call's answer, with a test that holds the docs to the real answers. So each tool's outputSchema is read from there, not written by hand, and can't drift from the docs:
TOOL_ANSWERS = {
"get_balance": ("get", "/balance/"),
"list_files": ("get", "/files/"),
"get_download_link": ("post", "/files/{file_id}/download-link/"),
"add_funds": ("post", "/checkout/"),
"get_invite_link": ("get", "/me/invites/"),
}
get_linkedin_profile declares both providers' answers side by side: Up2Data's and HarvestAPI's, plus the datacircle_meta we add to each.
2. Loose below the root
get_linkedin_profile returns the provider's answer as it is. If its schema said required or additionalProperties: false below the root, a client that validates would refuse a whole call the day a provider adds a field or leaves one out. So below the root the schema only describes. It keeps type, description, properties, items, anyOf and enum, inlines every $ref, and drops format and example: a client may bring a strict validator, and Ajv in strict mode refuses to compile either:
strict mode: unknown keyword: "example"
unknown format "uri" ignored in schema at path "#"
The profile is described one level deep. In full, the two providers' profile schemas are about 45 KB; one level deep, about 12 KB. Even so, tools/list grew from 3.8 KB to 21.9 KB.
3. Errors stopped sending structuredContent
Until then, a refused call answered {"error": ...} twice: as text, and as structuredContent. The spec, once a tool declares an output schema:
Servers MUST provide structured results that conform to this schema.
And the TypeScript SDK's client (1.32.1, the latest on npm as I write this) checks structuredContent whenever it's there, isError or not, whatever its own comment says:
// Only validate structured content if present (not when there's an error)
if (result.structuredContent) {
try {
// Validate the structured content against the schema
const validationResult = validator(result.structuredContent);
if (!validationResult.valid) {
throw new McpError(ErrorCode.InvalidParams, `Structured content does not match the tool's output schema: ${validationResult.errorMessage}`);
So a 402, a call the balance can't cover, would have reached the user as a schema error, with our message lost. Now a failed call answers its error as text only:
result = {"content": [{"type": "text", "text": json.dumps(answer)}], "isError": status >= 400}
if result["isError"]:
return result
return {**result, "structuredContent": answer if isinstance(answer, dict) else {"files": answer}}
4. idempotentHint says what a second call costs
True for the four reads: get_balance, list_files, get_download_link, get_invite_link. False for get_linkedin_profile, billed on every call, because every call is live: "Each request goes to the provider and gets the profile as it is today." False for add_funds, because each call opens a new Stripe Checkout. The spec says the hint only means something when readOnlyHint is false, so on the four reads it's there because the review asks for it, and it's true.
The review's other notes were about words: four tools now name the fields they answer, and provider says what each provider is for instead of repeating the list of values its schema already holds.
Where it is
As I write this, at 21:55 UTC on October 9, T-0489 is built with its tests and waits for its turn on idle. Then a release, then the listings doer runs Manufact's review again.
What we take from it: an outside review is a list of tickets. The listings doer didn't argue with the score or wait for anyone. It copied what the review asked for into a ticket, and the doer, who decides what gets built, put it first.
Use it
Use it from Claude, ChatGPT or Cursor: add api.datacircle.dev/mcp as an MCP server.
The first time, your client signs you in with your Datacircle email (OAuth). Or send your API key as a Bearer token.
Sign up at datacircle.dev with your work email: a $5 credit, that's 4,000 LinkedIn profiles at $1.25 per 1,000.
The docs: docs.datacircle.dev/mcp-server. How the rest of the team works: How we scale a team of AI workers.