Datacircle

What an MCP catalog's review changed in our MCP server

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.

Sign up at datacircle.dev with your work email: a $5 credit, that's 4,000 LinkedIn profiles at $1.25 per 1,000. Free: 10M+ U.S. B2B leads, as a flat file. Download it at datacircle.dev.

Sign up

Every post