DEV Community

Cover image for My MCP integration got rejected. Almost nothing in the server had to change.
Eugeniya Ivanova
Eugeniya Ivanova

Posted on

My MCP integration got rejected. Almost nothing in the server had to change.

Client quirks require unauthenticated tool lists

In July I set out to add our MCP server to the ChatGPT app directory. The server was already live, running in Claude and Cursor, with OAuth working. Submitting looked like a formality: describe what already exists, fill in the application, hit Submit.

A month and one rejection later, almost nothing had changed in the server itself. It had been working the whole time. What we ended up fixing was everything around it: how ChatGPT discovers the tools, what the scanner expects, what we'd written in the descriptions, and whether a reviewer could actually log in.

Here's what caught me.

The tool list that freezes solid

The first problem came before submission. I connected the server to ChatGPT — the connector was created, but it showed "no actions available." Zero tools. In Claude and Cursor the same tools loaded fine.

I spent a while looking at SSE and caching. Neither was the problem. ChatGPT reads the tool list when the app is created, and if the server returns zero at that point, the app can get stuck there. "Refresh" is disabled at zero and "Save" is greyed out. In my case, the way out was to delete the app and create it again.

The server returned zero because it required a token for tools/list itself. Claude logs in first and fetches the list afterwards, which is why we'd never seen the problem there. ChatGPT fetches the list before authorization, gets nothing, and turns to stone. So now I check one thing before doing anything else: tools/list needs to return 200 with the tools and no token.

Annotations the scanner demands and the spec doesn't

Next, OpenAI's scanner went through every tool and required explicit readOnlyHint, openWorldHint and destructiveHint values. Four read-only tools didn't have destructiveHint, which made sense according to the MCP spec: it only matters when readOnlyHint: false.

The scanner wanted it anyway. We added destructiveHint: false to those tools and wrote justifications for forty-odd annotation values. The tools themselves didn't change. We just had to make explicit something the spec allowed us to leave implied.

The domain challenge that wants a bare string and lives forever

A small one that's easy to waste time on: OpenAI's domain verification expects the endpoint to return a bare token string. No JSON, no quotes. Return {"token": "..."} and it fails.

It also isn't a one-time verification endpoint. It has to stay there in production, answering the same way afterwards. So we now have a tiny endpoint with one permanent job: return a string when OpenAI asks.

The drift I caught on myself

Then I found something that actually was our mistake.

Our public mcp-server repo defines 18 tools, four of them for LinkedIn analytics: post stats, account stats, followers and profile summary. The live server exposes 16, and none of those analytics tools are among them. They exist in the REST API, but aren't exposed through MCP.

The public repo had fallen behind the live server, and the app description was based on the repo. So we were describing analytics that someone using MCP couldn't actually access. I removed them from the description and release notes.

Then I found the same old description in two more places: the README for the Zed extension and the PR for the Docker MCP Catalog. The Zed PR was also pointing at a commit from before the correction, so merging it would have put the outdated claim into another catalog. We'd updated the product and missed a few places where we'd described it. Easy enough to do when those descriptions live in different repos and submissions.

The rejection that wasn't about the code

Submitted August 5, every wizard step completed in one pass. Rejected August 24.

The message was: "We're unable to complete your sign-in or OAuth flow… ensure valid, working credentials… no additional setup or verification."

Before changing anything, I walked through the flow again. Dynamic client registration returned 201. /authorize sent me to our consent screen. The sign-in page in a clean browser was a normal email-and-password form with no captcha. tools/list without a key returned the tools. API and MCP access were enabled on every plan, including free. Everything worked.

Then we looked at the account we'd given the reviewer.

Our own accounts use Google sign-in. A reviewer can't use our Google account, and even if they tried, the second factor would land on our phone. We'd managed to give them credentials for an account they had no way to get into.

Even worse, Canva had rejected us for the same reason before. Twice was enough to stop calling it bad luck. We simply didn't have "can a stranger actually use these credentials?" on our submission checklist.

What actually fixed it

We created a separate reviewer account: email and password, email confirmed ahead of time, a couple of channels connected, with some posts and drafts already there. We also included eight test cases — five positive and three negative.

The most useful one was a full write-path test through publora-playground. It's a fake publishing target: it validates the post against the same rules as a real one and returns the same kind of response, but throws the request away at the end instead of publishing it anywhere. A reviewer can go through the whole publishing flow without putting a test post on someone's actual timeline.

It took about thirty lines and ended up being one of the most useful things we added for the submission. The reviewer could finally test writes without needing a disposable social account.

We resubmitted on the evening of August 24. Approved September 4.

What I wrote down for next time

The main addition to my submission checklist is pretty basic: go through the product as the reviewer, not as yourself.

The server worked in Claude and Cursor. It followed the spec. OAuth worked when we tested it. But ChatGPT asked for the tool list in a different order, its scanner expected metadata we hadn't needed elsewhere, and the reviewer needed a login that didn't depend on being us. Those were the things that took the month.


I did this with Claude — it wrote the code and configs, while I checked what was happening and decided what needed changing. The funny part is that almost none of those decisions involved changing the server itself. Most of the work ended up being metadata, descriptions and access.

What's gotten your product bounced from a platform review — something actually broken, or something you never thought a reviewer would run into?

Top comments (18)

Collapse
 
reidmarlow profile image
Reid Marlow

The sandbox target validating writes without committing side effects is the smartest thing on this list. When an MCP server exposes state-mutating tools, giving reviewers a mock sink that still runs the real parameter validation avoids needing disposable test accounts, while also doubling as an integration test in CI.The multi-catalog drift across Zed, Docker, and ChatGPT manifests is the other headache that bites hard. Once tool schemas are maintained manually in more than one place, a renamed parameter or dropped tool inevitably rots in some README. I ended up writing a quick CI check that diffs catalog manifests against the server's live reflection output, which catches mismatched tool lists before anyone opens a submission PR.

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova

Two people in this thread landed on the same CI check independently — diffing the catalog manifests against the server's live reflection — which is a pretty strong sign that's the actual fix, not just remembering to update more READMEs.

And you're right that the playground doubles as an integration test — I'd only been thinking of it as a reviewer convenience. Same mock sink, run in CI, and every state-mutating tool gets its real validation exercised on every push without touching a real account. Adding both. Good thread to have posted this into.

Collapse
 
arkforge-ceo profile image
ArkForge

The tools/list without-auth requirement is the sharpest client divergence to know about: Claude and Cursor both authenticate before requesting the tool list, so discovery-behind-a-token works fine there and fails silently elsewhere. The safest default is treating tools/list as a public menu and auth as a gate on execution, not on discovery. The annotation exhaustiveness issue - scanner requiring destructiveHint: false even when the spec allows omitting it on read-only tools - suggests ChatGPT's submission pipeline is running stricter validation than the protocol itself mandates, which is worth anticipating early if you're targeting any other app directory with a similar scanner step.

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova

"Discovery is a public menu, auth gates execution" is the cleanest way to say it — I'm stealing that framing. It's obvious in hindsight, but the spec lets you gate discovery too, so nothing tells you it's a bad idea until a client that reads the menu first walks away with an empty plate.

And yeah — the scanner being stricter than the protocol is the thing I'd warn anyone about before their next directory. Passing the spec isn't the bar; passing someone's stricter reading of it is. I'm now assuming every catalog has its own extra layer and building the annotations to the strictest one I've hit, rather than the one the spec technically allows.

Collapse
 
vinhnguyenthanhdn profile image
Vinh Nguyen

The drift half looks like it is still open in the artifact the other descriptions were derived from. src/index.js on main has 18 server.tool registrations, and the four you named are all still among them — linkedin_post_stats, linkedin_account_stats, linkedin_followers, linkedin_profile_summary — with the last push dated 2026-08-04, before the rejection. docs/tools/overview.md heads that group "LinkedIn Analytics (6 tools)", counting the two reaction tools with them, so it is a third independently written count sitting in the same repo. That matters more than a stale README because the repo is what the Zed extension and the catalog PR were copied from, so the next person describing the server re-derives the original claim rather than reading your corrected release notes. The useful part is that your first fix makes this one mechanical: once tools/list has to answer 200 with no token, the live list becomes machine-readable authority, so a CI step diffing the registered names against it turns "we described tools an MCP user cannot reach" from a review finding into a failing build.

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova

Good catch — you're right that the repo itself still has all 18, so I fixed the descriptions and not the thing they're copied from. The "Analytics (6 tools)" heading is a third count in one repo, which I'd missed entirely.

The CI idea is the real fix though: once tools/list answers 200 with no token, the live list is machine-readable, so a build step diffing the registered names against it turns this into a failing build instead of a review finding. Adding that. Thanks — genuinely useful.

Collapse
 
pushpendra_agrawal_f1bdfa profile image
Pushpendra Agrawal

The tools/list-before-auth thing is the one that'll bite the most people. It's such a small ordering difference between clients but it turns "works everywhere" into "works nowhere" for one specific platform. We're building an MCP discovery layer and this is exactly the class of problem that never shows up in the spec, only in the gap between what different clients assume. The reviewer-account point is underrated too. Testing as yourself is the default failure mode for basically every integration, not just MCP.

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova

Yeah, "the gap between what different clients assume" is exactly it — the spec says what's allowed, not what each client actually does with that freedom. Claude and ChatGPT both read the spec correctly and still ended up on opposite sides of one ordering choice. A discovery layer sitting in that gap sounds genuinely useful, because right now everyone rediscovers these differences one rejection at a time.

And yeah — "test as yourself" is such a quiet default. You never notice you're doing it, because for you the thing works. Curious what you're building, MCP discovery is a good place to be right now.

Collapse
 
pushpendra_agrawal_f1bdfa profile image
Pushpendra Agrawal

The "tools/list before auth" quirk is the one that'll keep biting people, because it's not a client bug, it's a real disagreement about what tools/list means. ChatGPT treats it as "what could this server ever offer," Claude treats it as "what can this authenticated session offer." Both are defensible reads of an unauthenticated endpoint. We hit a version of this building an MCP layer across 1,500+ third-party apps: some upstream APIs literally can't tell you what's callable until you're authed (permission-scoped endpoints), so an unauthenticated tools/list is either a lie (static list, may not match reality) or empty (which ChatGPT then can't recover from per your post). There's no spec answer to that yet, only client-specific workarounds. Curious whether OpenAI's scanner team has said anything about fixing the empty-list dead-end rather than pushing servers to fake a static list.

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova

You've put your finger on the thing my post quietly assumed away. My advice — "make tools/list answer 200 without a token" — only works because our tool list is static. For a permission-scoped server it falls apart exactly the way you describe: the unauthenticated list is either a lie or empty, and ChatGPT can't recover from empty. So "just expose it publicly" isn't a fix there, it's a choice between two wrong answers.

"What could this server ever offer" vs "what can this session offer" is the cleanest framing of the disagreement I've seen. And the spec genuinely doesn't resolve it — it says the endpoint can require auth, without saying what a client should do when it does.

No, I haven't seen anything from OpenAI's scanner team about the empty-list dead-end — and honestly that's the part worth pushing on more than the static-list workaround. Recovering from an empty discovery is a client problem they can fix once; asking every permission-scoped server to fake a static list is a workaround they're pushing onto everyone forever. If you ever get a channel to them on this, I'd love to know what they say.

Collapse
 
pushpendra_agrawal_f1bdfa profile image
Pushpendra Agrawal

The reviewer-account thing is the part people miss most. You can pass every spec check and still fail because you tested as yourself, not as a stranger with no context. That's basically the whole trust problem with MCP servers in miniature: works great when the person running it already knows how it's supposed to behave, breaks when someone with zero prior context tries to use it. We're seeing the same pattern building a marketplace for these servers, the technical spec compliance is the easy 80%, the "can an outsider actually operate this safely" part is where everything interesting (and broken) lives.

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova

Yeah — "works when you already know how it should behave" is exactly the trap, because the person who built it can't un-know how it works. You test it and it passes, because you're the worst possible reviewer: you already know all the parts you'd otherwise get stuck on. A stranger with no context is the only honest test, and they're the one person you can't easily be.

The 80/20 split matches what I saw too. Spec compliance was a checklist I could finish. "Can someone with no idea what Publora is operate this without help" had no checklist, and that's where the whole month went. Curious what the marketplace side looks like — you're seeing this across a lot of servers at once, which is a vantage point I don't have from inside one.

Collapse
 
kalashnikovisme profile image
Pavel Kalashnikov

Great work!

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova

Thanks!

Collapse
 
svgicons profile image
Svg/icons

The tools/list ordering difference is a great example of why protocol compliance and real client interoperability need separate preflight tests

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova

Exactly — passing the spec tells you the server is correct, not that any given client will accept it. Those turned out to be two different green checkmarks, and I only had a test for the first one. A preflight that actually runs the real client's discovery order would've caught this before submission instead of after a rejection.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.