Deep Dive

I Went from 64 to 100/100 on Smithery.
Here's Every Fix.

A complete account of every quality dimension that separates a working MCP server from a well-built one, plus the skill that automates all of it.

April 2026  ·  15 min read  ·  seasons.kooexperience.com

MCP Claude Code TypeScript Smithery Open Source
01
What is MCP?
The short version: it's how you give AI clients access to your live data.

Before MCP, an AI assistant only knew what it was trained on plus whatever you pasted into the chat. If you wanted it to check your database, call an API, or get a live weather reading, you were doing that yourself and copy-pasting the result. That workflow breaks at scale and breaks badly when the data changes fast.

MCP — Model Context Protocol — is an open protocol that lets AI clients call external tools and data sources directly. You write a server that exposes tools. AI clients discover and call those tools at inference time. The AI gets real, live data. You stop copy-pasting.

It's also not proprietary. Anthropic published the spec, and most major clients have picked it up — Claude, Cursor, Windsurf, Cline. Write one server and it runs in all of them.

The mental model: MCP is to AI clients what REST APIs are to web apps. It's a standard interface. Clients don't need to know how your server is built. They just call the tools.
AI Client
Claude / Cursor / Windsurf
MCP Server
Your tools + logic
Your API / Data
Live, real-time

Real examples already in production: the GitHub MCP lets Claude read and write code in your repos. The Stripe MCP lets it query payment data. Internal database MCPs at companies give their AI assistants direct access to proprietary data that never hit training. The pattern is the same everywhere — expose your data as tools, let the AI call them.

The protocol handles tool discovery, parameter validation, error responses, and streaming. You focus on writing the tools themselves.

02
Why Build Your Own?
Four reasons that actually hold up in practice.
Your data, live
If you have an API or database, MCP gives AI clients direct access without any manual copy-paste step. The data is always current. You don't maintain a separate sync pipeline.
Works everywhere
One server, every client. Claude Desktop, Cursor, Windsurf, and any other MCP-compatible tool can use it without modification. The protocol is the integration layer.
Discoverable
Smithery, mcp.so, Glama, and PulseMCP index MCP servers like npm indexes packages. Build it once, publish it, and people find it through their tool of choice without you doing anything else.
Replaces custom integrations
Before MCP, you'd write a bespoke integration for each AI tool separately — once for Claude, once for Cursor, once for your internal Slack bot. One MCP server beats five separate integrations, and you maintain one codebase.
03
What I Built
japan-seasons-mcp — a live data server for Japan seasonal travel.

I built seasons.kooexperience.com because I was tired of asking AI assistants about sakura bloom timing and getting responses based on averages from years ago. The actual bloom date shifts by weeks depending on the year. You need live data.

The MCP server wraps the JMA (Japan Meteorological Association) and n-kishou APIs and exposes them as callable tools. Ask Claude when cherry blossoms will peak in Kyoto this year, it calls sakura.forecast, gets current data, and gives you a real answer.

12
Tools
1,700+
Spots tracked
6
Data types
100
Smithery score

Six data types: sakura and koyo forecasts from the Japan Meteorological Corporation (live, updated daily), short-range weather from JMA (live), and curated static datasets for flowers, fruit farms, and festivals — 1,700+ GPS-tagged locations in total.

100/100 on Smithery. The server is listed at smithery.ai and achieves a perfect quality score. Getting there took about 15 specific fixes — all covered in the sections below.
04
The 10 Quality Dimensions
Smithery scores servers across ten dimensions. Here's what each one checks.

When I first submitted to Smithery I got a 64. I could see the score but not the breakdown. That forced me to read the spec carefully and reverse-engineer what each dimension actually checks.

Dimension Points What it checks
Tool descriptions 12 Every tool has a clear, verb-first description that names the resource and explains when to call it
Parameter descriptions 11 Every parameter has a description, type, and example where relevant — nothing left implicit
Annotations 7 Tools declare readOnlyHint, destructiveHint, openWorldHint — signals for how clients should treat the tool
Tool names (dot notation) 5 Tool names use domain.action format to form a navigable tree (e.g. sakura.forecast, not get_sakura_forecast)
Prompts 5 Server exposes at least one prompt that helps users understand how to interact with the server
Resources 5 Awarded automatically — Smithery's own tooltip says "Resources are always awarded full points"
Server metadata 30 Complete smithery.yaml with name, description, homepage, categories, and tags
Config UX 25 Config schema is minimal — no unnecessary required fields, sensible defaults for optional params
Server instructions varies The server sends an instructions string in initialize response — routing guidance for the AI
Static data & caching bonus Frequently-called data is cached with appropriate TTLs — reduces upstream API load and latency
The resources dimension is free. Smithery awards those 5 points automatically. Don't implement custom resources just for the score — it won't change anything.
05
The Score Journey
Five stages from 64 to 100. What broke and what fixed it.

Stage 1 — First submission: 64/100

The server worked. All 12 tools returned correct data. But the quality score told a different story. What was actually wrong:

  • Tool descriptions were generic one-liners — "Get sakura information for Japan" — no verbs, no specifics
  • Parameters had no descriptions at all, just names and types
  • No annotations — clients had no signal on read vs write, safe vs destructive
  • Tool names were all get_* — no dot notation, no domain grouping
  • No smithery.yaml beyond the bare minimum
  • No server instructions
Starting score64/100

Stage 2 — Tool descriptions + parameter descriptions + annotations: ~82

Rewrote every tool description to be verb-first, specific, and useful. Added full parameter descriptions with types and examples. Added annotations to every tool using four constants (covered in section 8). Jumped 18 points in one pass.

The parameter descriptions alone were probably worth 8 of those points. I had 40+ parameters across 12 tools with no descriptions. That's a lot of missing signal.

After descriptions + annotations~82/100

Stage 3 — Server instructions + prompts: ~90

Added an instructions string to the initialize response. This is the routing guide — it tells the AI when to use which tool, in what order, and for what kind of question. Wrote it like internal documentation for the model, not for humans.

Also added two prompts: one for sakura season planning, one for koyo. These are pre-built interaction patterns that users can invoke directly from supported clients.

After server instructions + prompts~90/100

Stage 4 — smithery.yaml + full metadata: ~98

Wrote a complete smithery.yaml with name, description, homepage URL, categories (travel, weather, japan), tags, and a minimal config schema. The config UX dimension rewards you for keeping the setup friction low — no required API keys for a public read-only server means full marks there.

After smithery.yaml + metadata~98/100

Stage 5 — Dot notation tool names: 100

Stuck at 98 for longer than I'd like to admit. The fix was the last 2 points and the most non-obvious change in the whole process. Full story in the next section.

After dot notation rename100/100
06
The Dot Notation Discovery
Stuck at 98 for days. The fix was hiding in a tooltip.

After stage 4, I was at 98. I knew I was missing 2 points somewhere in the tool naming dimension. I'd read the docs. I thought my names were fine. They followed the pattern, had underscores, were descriptive. Standard stuff.

I asked Grok to review the tool names. It suggested varying the verb prefixes — replace all get_ with a mix of list_, find_, and fetch_ depending on whether the tool retrieves a collection or a single item. That made semantic sense, so I tried it. Still 98.

Then I hovered the tooltip on the tool names section in the Smithery dashboard. The text read: "Tool names should form a navigable tree using dot-notation (e.g. admin.tools.list)".

That was it. Not underscores. Dots. The dimension is checking for a grouped, hierarchical naming scheme — the kind that lets a client build a tree view of your tools instead of a flat list of 12 similarly-named functions.

Before — flat snake_case
get_sakura_forecast
get_sakura_spots
get_sakura_best_dates
get_koyo_forecast
get_koyo_spots
get_koyo_best_dates
get_kawazu_cherry
get_weather_forecast
After — dot notation
sakura.forecast
sakura.spots
sakura.best_dates
koyo.forecast
koyo.spots
koyo.best_dates
kawazu.cherry
weather.forecast

Renamed all 12 tools. Resubmitted. 100.

Why it matters beyond the score: The dot notation groups tools by domain. When an AI client lists your tools, it sees sakura.*, koyo.*, weather.* as natural clusters — not a flat list of 12 equally-weighted names. The model can route more accurately because the naming itself is structured signal.

Here's what a tool registration looks like with the current SDK:

TypeScript — registering a tool with dot notation
server.registerTool( "sakura.forecast", { title: "Sakura Forecast", description: "Get live cherry blossom bloom progress for 48 cities across Japan. " + "Returns bloom percentage, status, and peak date estimates. " + "Call this first for city-level timing, then use sakura.spots for specific parks.", inputSchema: { prefecture: z.string().optional() .describe("Optional prefecture name in English. Example: 'Tokyo', 'Kyoto', 'Hokkaido'."), }, annotations: READONLY_EXTERNAL, }, async ({ prefecture }) => { try { const data = await getOrFetch(`sakura:${prefecture ?? "all"}`, TTL.FORECAST, () => fetchForecast(prefecture) ); return { content: [{ type: "text", text: JSON.stringify(data) }] }; } catch (err) { return { isError: true, content: [{ type: "text", text: `Failed: ${err instanceof Error ? err.message : String(err)}` }], }; } } );
07
Tool Description Quality
Worth 12 points. Also the single biggest impact on how accurately the AI routes tool calls.

The description is what the AI reads to decide whether to call your tool and what to pass it. A bad description leads to wrong tool selection, wrong parameter choices, or the model hallucinating data instead of calling your tool at all. The score dimension rewards quality here because it correlates directly with usability.

Bad

"This tool provides information about sakura spots in Japan."

Good

"Get live bloom percentages for 1,012 cherry blossom spots across Japan. Filter by prefecture. Call sakura.forecast first for city-level timing, then use this for specific parks."

Four rules for tool descriptions

Parameter descriptions matter just as much. For a parameter like prefecture, "Prefecture to filter by" is nearly useless. "Optional prefecture name in English or Japanese to filter results. Example: 'Tokyo', 'Kyoto', '東京'" tells the model the format, the optionality, and gives a concrete example. That's the difference between the model guessing and the model knowing.
Good parameter description in schema
{ type: "object", properties: { prefecture: { type: "string", description: "Optional prefecture name in English or Japanese. " + "Example: 'Kyoto', 'Hokkaido', '北海道'. " + "Omit to return all prefectures.", }, year: { type: "number", description: "Four-digit year for historical data. Defaults to current year. " + "Range: 2010–2026.", }, }, required: [], }
08
Annotations and Caching
Seven points and a production necessity. Define these once and reuse everywhere.

Annotations are metadata attached to each tool that tells clients how to treat it. A client that knows a tool is read-only can call it without asking for user confirmation. A client that knows a tool is destructive should warn the user. These signals also flow through to logging, rate limiting, and audit trails.

I defined four constants and apply them to every tool. No guessing, no inconsistency:

TypeScript — annotation constants
// Read-only: safe to call repeatedly, no side effects, data from cache or internal state export const READONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, } as const; // Read-only with external calls: fetches from upstream APIs, may have latency/rate limits export const READONLY_EXTERNAL = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, // signals that this tool calls outside the local environment } as const; // Write: modifies state, idempotent (calling twice has the same effect as once) export const WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, } as const; // Destructive: deletes or irreversibly modifies — clients should prompt for confirmation export const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, } as const;

For japan-seasons-mcp, every tool uses either READONLY or READONLY_EXTERNAL. Tools that return cached static data (prefectures, flower types) use READONLY. Tools that make live API calls use READONLY_EXTERNAL — the openWorldHint: true flag is the signal.

Caching

One tool call without caching means one upstream API request. With 12 tools and potentially dozens of calls per session, that adds up fast. More importantly, most of the data doesn't change on a per-request basis — bloom percentages update once or twice a day, not on every query.

I defined TTL constants that match the actual update cadence of each data type:

TypeScript — cache TTL constants
export const TTL = { WEATHER: 30 * 60 * 1000, // 30 minutes — forecast changes frequently FORECAST: 4 * 60 * 60 * 1000, // 4 hours — bloom progress updates daily, but check often DAILY: 6 * 60 * 60 * 1000, // 6 hours — best dates, spot data — updated once or twice daily STATIC: 365 * 24 * 60 * 60 * 1000, // 1 year — prefecture lists, flower types, static metadata } as const;
Don't cache weather too aggressively. Weather data for Japan in sakura season changes hourly. A 30-minute TTL is already aggressive — go longer and you'll serve stale rain forecasts that affect bloom viewing recommendations.

The cache layer is a simple in-memory Map with timestamp-based invalidation. No Redis, no external dependency. For a server that runs in a single process and handles read-only data, that's all you need.

09
The create-mcp Skill
A Claude Code skill that encodes all of this so you go straight to 100.

After going through the whole process manually, I wrote a Claude Code skill that captures every lesson. A skill is a markdown file that gives Claude a structured workflow — it loads into context and guides the session with specific steps, checklists, and patterns to follow.

The create-mcp skill has two paths:

Both paths encode every fix covered in this post — dot notation naming, verb-first descriptions, annotation constants, TTL strategy, smithery.yaml structure — so you're not discovering them one at a time by watching your score move.

Gather
API spec, auth, data shape
Design
Tools, names, grouping
Build
TypeScript + cache
Audit
All 10 dimensions
Publish
5 directories

The audit step is where most of the value is. It runs through a checklist of every dimension — tool names (dot notation check), descriptions (verb-first check, max 2 sentence check), parameters (description present, example present), annotations (type assigned), server instructions (present and complete), smithery.yaml (all fields filled), prompts (at least one), resources (hosted registry check).

Install it with:

Install create-mcp skill
curl -fsSL https://raw.githubusercontent.com/haomingkoo/create-mcp/main/install.sh | sh

Once installed, invoke it in any Claude Code session with /create-mcp. It will ask whether you're creating or auditing, then walk you through the full workflow. Source and full documentation on GitHub.

The skill encodes the fixes. The dot notation rule, description format, annotation constants, TTL values, smithery.yaml structure — all of it is in the skill's workflow. Whether you're auditing something that already exists or building from scratch, you run through the same checklist once.
10
Does the Skill Actually Work?
Three independent scenarios, graded against 34 quality assertions.

Writing a checklist is easy. The test is whether it holds up on real tasks. I ran it against three independent scenarios — different APIs, one audit of a broken server — and graded the outputs against explicit assertions with a baseline for comparison.

Scenario Type Pass rate Time
CoinGecko crypto MCP Create 11/12 (92%) 133s
Recipe MCP audit Audit 10/10 (100%) 95s
OpenWeatherMap MCP Create 12/12 (100%) 110s

The one miss in the crypto eval was an ambiguous assertion about TTL constants — the grader read "TTL inside the handler body" as requiring the constant to be defined in the handler, not just used there. The actual code was correct: getOrFetch(key, TTL.PRICES, fn) called in every handler. Wording issue, not a skill issue.

The audit scenario is where the previous version of the skill had a real gap. It would identify all the problems and describe the fixes — but not always write package.json and the smithery config as output files. The revised skill makes this explicit: generate these files as output, not as a suggestion. The audit eval now verifies both files physically exist in the output directory.

The new assertions that discriminate. The baseline (older skill) failed on two checks the new assertions added: package.json values must not be placeholder text (the old skill left "author": "Your Name" and GitHub placeholder URLs), and prompt message text must name at least one tool by its dot notation name. The new skill passes both. The only useful assertion is one a bad output can't accidentally pass.
3
Scenarios tested
97%
Avg pass rate
113s
Avg generation time
34
Total assertions
11
Getting Discovered
Five directories. Most of them take less than 10 minutes.

Publishing to Smithery is the highest-leverage directory because it scores your server, shows up in the most MCP clients, and has the most users. But the others are worth doing — they each have different audiences and some are crawled by AI search tools directly.

Do you need npm?

It depends on how your server runs.

Hosted servers (HTTP, deployed on Railway/Fly.io) don't need npm at all. Users connect directly to your URL — Smithery stores the endpoint and handles the connection. You push to GitHub, Railway deploys, users get the update automatically. japan-seasons-mcp works this way. No npm, no user installs.

Local servers (stdio, runs on the user's machine) need npm. When a user adds your server to Claude Desktop, the config looks like this:

claude_desktop_config.json — stdio server entry
{ "mcpServers": { "my-weather": { "command": "npx", "args": ["my-weather-mcp"] } } }

Claude Desktop spawns npx my-weather-mcp as a subprocess whenever it starts. npx fetches your package from the npm registry and runs it — that's the entire install story from the user's side. No manual setup, no cloning, no build step. But it only works if the package is on npm.

To publish:

Publish stdio server to npm
npm run build # compile TypeScript to dist/ npm publish --otp=123456 # one-time password from npm account

Users get the latest version on each run — npx checks for updates automatically. Push a fix to npm and it rolls out without anyone touching their config.

Which should you build? If your data is public and doesn't vary per user, hosted HTTP is simpler — one deployment, zero user installs. If your tool needs access to the user's local files, localhost services, or private credentials, stdio is the right choice.

Hosting a server on Railway

Railway is the easiest way to get a hosted MCP live. Connect your GitHub repo and it deploys on every push — no infra to manage.

  1. Push your server to GitHub
  2. Go to railway.app → New Project → Deploy from GitHub repo
  3. Set the start command: node dist/index.js
  4. Add environment variables (API keys, etc.) in the Railway dashboard
  5. Copy the public URL Railway assigns (e.g. https://your-app.up.railway.app)

That URL is your MCP endpoint. Register it on Smithery once:

Register hosted endpoint on Smithery
npx @smithery/cli mcp publish \ "https://your-app.up.railway.app/mcp" \ -n your-github-username/your-repo \ --config-schema "$(cat smithery.remote-config.json)"

After that, every git push to main triggers a Railway redeploy. Everyone connecting via Smithery or the direct URL gets the update automatically — no version bumps, no user action required. This is how japan-seasons-mcp ships updates: push the code, Railway deploys in under a minute, and users connecting via Smithery or the direct URL get the new version on their next call.

Directory Setup Notes
Smithery CLI Highest traffic. Scores your server. Hosted registry gives you the free resources points.
mcp.so UI Submit via their web form. Simple listing, no scoring. Gets indexed by Google.
Glama CLI Submit via GitHub PR to their registry repo. Fast review, usually merged same day.
PulseMCP UI Web submission form. Categorized directory, good for niche discovery.
mcp.run UI Smaller community but active. Good for getting early feedback and stars.

For Smithery specifically, if your server is hosted (not just directory-listed), you can deploy updates with:

Smithery CLI — publish hosted server
npx @smithery/cli mcp publish \ "https://your-domain.com/mcp" \ -n your-github-username/your-repo \ --config-schema "$(cat smithery.remote-config.json)"

For stdio servers (installed via npx), go to smithery.ai → Add Server → paste the GitHub URL. It picks up the smithery.yaml automatically.

Submitting to all five directories takes 30–60 minutes total. Each one wants roughly the same information — name, GitHub URL, a one-paragraph description — just in slightly different forms.

12
That's the Whole Thing

Building MCPs is straightforward. The SDK is well-documented, TypeScript support is solid, and the deploy story (Railway, Smithery hosted, or self-hosted) is mature enough. A working MCP server is an afternoon of work.

The gap between a working MCP and a well-built one is about 15 specific things: dot notation naming, verb-first descriptions, parameter examples, annotation constants, server instructions, a complete smithery.yaml, at least one prompt, appropriate TTLs, minimal config surface area, and submitting to all five directories instead of just one.

This post covers all of them. The create-mcp skill automates most of them.

If you're building your own, the japan-seasons-mcp source is on GitHub and has all of these patterns in production. Feel free to use it as a reference.

Live demo: The MCP server is running now at seasons.kooexperience.com. If you're in a sakura season window, ask Claude when cherry blossoms will peak in a city you're visiting. It'll call sakura.forecast and give you a real answer with current bloom data.
15
Specific fixes
+36
Points gained
100
Final score
5
Directories listed