Publishing Your MCP Server to NuGet.org and the MCP Registry
Once you've got a working MCP server tested locally against the Inspector or a real client, the next step is making it installable and discoverable by other people. That means two separate publishing targets: NuGet.org, which hosts the actual package, and the official MCP Registry, which is a searchable directory that points at packages hosted elsewhere — it doesn't host code itself.
This post walks through that full pipeline for a C# MCP server, including the specific errors you're likely to hit — most of which have short, non-obvious fixes once you know what's going on.
How the two pieces fit together?
It's worth understanding this relationship before starting, since it explains the order of operations:
- NuGet.org hosts your actual compiled package (the
.nupkg) — this is whatdotnet add packageordotnet tool installactually downloads. - The MCP Registry stores only metadata — name, description, version, and a pointer to where the real package lives (NuGet, npm, PyPI, or an OCI registry). It never hosts code.
Consequence: the registry can't be your first stop. You publish to NuGet first, then point the registry at it — and the registry will actually go fetch your NuGet package's README to verify you own it, which is the mechanism behind the "ownership marker" step below.
Preparing the .csproj for packaging
A few properties turn a normal console app into something NuGet recognizes as an MCP server package:
<PropertyGroup>
...
<!-- MCP / NuGet packaging -->
<PackageType>McpServer</PackageType>
<PackAsTool>true</PackAsTool>
<ToolCommandName>your-tool-name</ToolCommandName>
<PackageId>YourCompany.McpServer</PackageId>
<Version>1.0.0</Version>
<!-- NuGet package metadata -->
<Authors>Your Name</Authors>
<Description>Short description of what this server does.</Description>
<PackageProjectUrl>https://github.com/you/your-repo</PackageProjectUrl>
<RepositoryUrl>https://github.com/you/your-repo</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.nuget.md</PackageReadmeFile>
<PackageTags>mcp;model-context-protocol;ai</PackageTags>
</PropertyGroup>
<ItemGroup>
<None Include="README.nuget.md" Pack="true" PackagePath="\" />
<None Include=".mcp/server.json" Pack="true" PackagePath="\.mcp\" />
</ItemGroup>
Two things worth explaining:
PackageReadmeFilepoints at a separate README from your repo's mainREADME.md. This is the file that renders on the NuGet.org package page — and, importantly, the file the MCP Registry fetches to verify ownership (next section). Keep it shorter and install-focused rather than reusing your full dev-oriented repo README.- The
<None Include>entries bundle your README andserver.jsoninto the actual.nupkgfile. Worth double-checking these actually made it in — a quick way istar -tf yourpackage.nupkgafter packing, to list the archive contents.
The ownership marker
The registry needs proof you actually control the NuGet package you're pointing it at. The mechanism: a hidden comment inside your NuGet README that has to exactly match the name field in your server.json.
# YourProject
<!-- mcp-name: io.github.your-username/YourRepoName -->
Short description of the server...
server.json's name field character-for-character, including casing. A mismatch here is the single most common reason publishing fails later, and it's easy to introduce by renaming your package ID or GitHub account partway through setup without updating every copy.
Writing server.json
This is the file the registry actually reads. Current schema as of this writing:
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"description": "Short description, under 100 characters.",
"name": "io.github.your-username/YourRepoName",
"version": "1.0.0",
"packages": [
{
"registryType": "nuget",
"registryBaseUrl": "https://api.nuget.org/v3/index.json",
"identifier": "YourCompany.McpServer",
"version": "1.0.0",
"transport": { "type": "stdio" },
"environmentVariables": [
{
"name": "YOUR_API_KEY",
"description": "Description of what this is for",
"isRequired": true,
"isSecret": true
}
]
}
],
"repository": {
"url": "https://github.com/your-username/your-repo",
"source": "github"
}
}
Two fields are worth calling out specifically, because both caused real validation failures in practice:
The description length limit
→ Fix: keep description at or under 100 characters. There's no warning at write time — this only surfaces when you actually validate/publish.
The registryBaseUrl format
→ Fix: use the full NuGet v3 API index URL (https://api.nuget.org/v3/index.json), not just the domain root. Easy mistake since the domain alone looks like a reasonable value.
Publishing to NuGet.org
- Create an account at nuget.org if you don't have one — free, sign in with a Microsoft account or GitHub.
- Check your package ID is available by searching for it on the site first. IDs are globally unique and, importantly, case-insensitive for reservation purposes — whichever casing you push first becomes permanent, so decide on casing before your first push, not after.
- Generate an API key under Account → API Keys → Create, scoped to "Push new packages and package versions," with a glob pattern matching your package ID.
- Pack the project:
dotnet pack -c Release - Push it:
dotnet nuget push bin/Release/YourPackage.1.0.0.nupkg \
--api-key YOUR_API_KEY \
--source https://api.nuget.org/v3/index.json - Wait for indexing — new packages usually appear in search within minutes, but full indexing (including README rendering) can take 15–20 minutes. Check the package page directly (
nuget.org/packages/YourPackage) rather than search, since the page updates faster.
curl "https://api.nuget.org/v3-flatcontainer/yourpackage/1.0.0/readme" (lowercase package ID in the URL). If the mcp-name comment isn't in that output yet, indexing isn't finished — wait before attempting the registry publish.
Getting the publisher CLI
Download mcp-publisher from the registry repo's Releases page, or build it from source with make publisher.
amd64 build, not arm64 (ARM64 Windows is uncommon — Surface Pro X and similar devices only). Running the wrong one fails immediately:→ Fix: check echo $env:PROCESSOR_ARCHITECTURE in PowerShell — if it says AMD64, download the amd64 release asset, not arm64.
Authenticating
Run this from your project's root directory:
mcp-publisher.exe login github
This opens a device-flow prompt — visit the URL shown, enter the code, approve via GitHub OAuth. No API tokens to generate or store yourself.
io.github.<username>/... to whichever GitHub account completes this OAuth flow — not to whichever account owns the actual source repository. Those can be different accounts entirely; the repository.url field in server.json is just descriptive metadata and isn't cross-checked against the authenticating identity. If you have multiple GitHub accounts logged into the same browser, double-check the active session before clicking Authorize.If your GitHub account is flagged
Occasionally an account gets restricted from authorizing third-party OAuth apps — GitHub shows a banner like "This account is flagged, and therefore cannot authorize a third party application." This is unrelated to MCP or NuGet; it's a GitHub-side account standing issue, usually triggered by a cluster of recent security events (new sign-in, new linked identity, device verification) that got flagged automatically, even when every action was legitimate.
Two paths forward: file a support ticket with GitHub directly, or — faster if you have one — authenticate with a different GitHub account. The second option works cleanly even if that account doesn't own the repository, per the note above.
Validating and publishing
server.json in your current working directory — not inside a .mcp/ subfolder, even though that's where the .csproj packages it from for NuGet. Keep two copies in sync: one at the project root (for the CLI to read locally) and one at .mcp/server.json (bundled into the .nupkg). Whenever you edit one, copy it to the other before validating.→ Fix: copy .mcp\server.json server.json, then re-run.
Validate first — this checks schema and does the same ownership-marker check as a real publish, without submitting anything:
mcp-publisher.exe validate
Then publish:
mcp-publisher.exe publish
Token expiry between validate and publish
If time passes between authenticating and actually publishing — for example, while fixing validation errors — the session token can expire:
→ Fix: logout, then login github again, then publish immediately after — don't let a long gap open up between re-authenticating and the actual publish call.
Deprecated schema warning
You may see a non-blocking warning if the schema version in your server.json has been superseded:
→ Fix: check the linked changelog before assuming a large migration is needed — often it's a single line (the $schema URL itself) with no structural changes required, especially for simple stdio/package-based servers. Update both copies of server.json and re-validate.
What it looks like when it works?
Publishing to https://registry.modelcontextprotocol.io...
✓ Successfully published
✓ Server io.github.your-username/YourRepoName version 1.0.0
Confirm it's externally visible:
curl "https://registry.modelcontextprotocol.io/v0/servers?search=YourRepoName"
Or browse directly to registry.modelcontextprotocol.io and search.
Publishing an update
For subsequent versions, the flow repeats but with two things to remember:
- NuGet version numbers are permanent — once pushed, a version can be unlisted (hidden from search) but never deleted or reused. Always bump
<Version>in the.csprojbefore packing again. - Keep
server.json's version in sync with whatever you just pushed to NuGet — both the top-levelversionand thepackages[0].versionfields. A mismatch here is a quiet source of confusion, since the registry cross-checks these.
RecapChecklist
- Package hosted on NuGet before attempting registry publish — the registry only stores a pointer, not code
- Ownership marker in the NuGet README matches
server.json'snamefield exactly descriptioninserver.jsonis 100 characters or fewerregistryBaseUrluses the full API index URL, not the bare domain- Downloaded the correct CPU architecture build of
mcp-publisher - Keep a synced copy of
server.jsonat the project root, not just under.mcp/ - Validate immediately before publishing to avoid token expiry between steps
- Version numbers bumped consistently across
.csprojand bothserver.jsoncopies for every release
