Engineering
How to publish an MCP server and get it listed.
I wrote the server in an afternoon. It took two months to get it listed, because the chain has a dependency nobody documents and I did the steps in the wrong order. Here is the order that works.
Nobody browses npm
Nobody browses npm looking for MCP servers. Discovery happens in the directories and the awesome lists, and those have requirements nobody documents. My first pull request sat untouched until a bot closed it after thirty days, and I only found out when I went looking.
So publishing is not the finish line. It is the middle.
Twenty lines and two dependencies
You need a bin entry, otherwise clients cannot run it with npx.
npm install @modelcontextprotocol/sdk zod{
"name": "@feedbug/mcp",
"bin": { "feedbug-mcp": "dist/index.js" },
"files": ["dist/*", "README.md"],
"scripts": { "build": "tsc", "prepublishOnly": "tsc" }
}A server is a name, a transport, and a list of tools.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
const server = new McpServer({ name: 'feedbug', version: '0.1.0' })
server.tool(
'feedbug_list_bugs',
'List bugs reported via the FeedBug widget.',
{
status: z.enum(['open', 'in-progress', 'resolved']).optional(),
limit: z.number().min(1).max(50).optional(),
},
async ({ status, limit }) => {
const result = await client.listBugs({ status, limit })
return { content: [{ type: 'text', text: JSON.stringify(result) }] }
},
)
await server.connect(new StdioServerTransport())That is the whole server. Two things in it matter more than the rest. The description is the prompt: the agent picks your tool from that sentence alone, so write what it returns rather than what it is. And the schema is the contract: describe every field, the agent reads those too.
Break it before an agent does
npm run build
npx @modelcontextprotocol/inspector node dist/index.jsThe inspector lists your tools and lets you call them by hand. Do this before publishing. A broken schema is completely invisible until an agent tries to use it, and then it just silently stops calling your tool.
Two commands, and you are live
npm login
npm publish --access publicUsers then install it with one line, and the client writes its own config.
{
"mcpServers": {
"feedbug": {
"command": "npx",
"args": ["-y", "@feedbug/mcp"],
"env": { "FEEDBUG_PROJECT_KEY": "pk_your_key" }
}
}
}Your server now works perfectly and nobody knows it exists.
The two things directories check
Do these before submitting anywhere, not after. A public GitHub repository, not npm: the lists reject npm links, they want source people can read. And a Dockerfile, because Glama runs its quality checks in a container: no Dockerfile, no complete score, and the score is what the lists display.
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json tsconfig.json ./
RUN npm install --ignore-scripts
COPY src ./src
RUN npm run build
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package.json ./
RUN npm install --omit=dev --ignore-scripts
COPY --from=build /app/dist ./dist
ENTRYPOINT ["node", "dist/index.js"]Glama, and the badge that lies
Add the server on glama.ai/mcp/servers with your GitHub URL. Submissions are reviewed before becoming visible; mine was approved the same day. Then claim it from the server page, because approval and ownership are two different things and only claiming unlocks the admin tab.
Here is the trap that cost me a month: the badge URL returns a valid image immediately, before your server has been evaluated. It renders, so you assume you are done. A badge that renders does not mean a score exists.
The pull request that survives
Only now, open the pull request. One line, in the right section, split here so it fits:
- [owner/repo](https://github.com/owner/repo)
[](https://glama.ai/mcp/servers/owner/repo)
📇 ☁️ - What it does, in one sentence.The emoji are a legend the README defines at the top: language, then local or cloud service. Read the legend rather than copying a neighbour. And rebase before opening. Mine collected merge conflicts while I ignored it, and that is what got it closed.
The order is the whole trick
Public repo, Dockerfile, Glama, claim, score, pull request. Every step depends on the one before it. In that order it is an afternoon. Out of order, it took me two months.
The server in this post is FeedBug's: visual bug reports, with their screenshot, diagnostics and DOM context, read straight from your coding agent.