Skip to main content
Glama
SravanthiGujjula

my-first-server

My First MCP Server

A minimal Model Context Protocol server that exposes a single tool, get_current_time, over stdio. Built with the TypeScript SDK.

Use it as a learning reference for how an MCP server is structured and how it connects to hosts like Claude Code and the Claude desktop app.

πŸ“š See docs/transports.md for notes on stdio vs HTTP transports, "Streamable HTTP," and the stateless spec update (2026-07-28) that removed Mcp-Session-Id and the handshake.

How it works

An MCP host (Claude Code, the Claude desktop app) launches this server as a child process and talks to it over stdin/stdout using newline-delimited JSON-RPC. The server advertises its tools; when the model decides to use one, the host sends a tools/call request, the handler runs, and the result flows back into the model's context.

Claude (host)  <--- JSON-RPC over stdio --->  node dist/index.js (this server)

stdio gotcha: stdout is the JSON-RPC channel. Never console.log in a stdio server β€” it corrupts the protocol stream. Log to stderr with console.error (visible via claude --debug or the Inspector).

Key files:

File

Purpose

src/server.ts

The tool definition (registerTool). Transport-agnostic β€” shared by both entrypoints.

src/index.ts

stdio entrypoint (what Claude Code / desktop launch).

src/http.ts

Streamable HTTP entrypoint (Express, stateless, POST /mcp).

src/client.ts

Demo client β€” connects to the HTTP server, lists + calls the tool.

.mcp.json

Registers the server with Claude Code (project scope, committed). Uses a relative path so it works on any machine.

package.json

prepare script auto-builds dist/ on npm install.

dist/

Compiled output. Git-ignored β€” regenerated by the build.

docs/transports.md

Notes on stdio vs HTTP transports and the stateless spec update.


Related MCP server: datetime-mcp

Setup

The Node version is pinned in .nvmrc. Select it first (installs it if needed):

nvm use         # reads .nvmrc; run `nvm install` first if that version is missing
npm install     # installs deps AND builds dist/ (via the "prepare" script)

nvm use + npm install is all a fresh clone needs before the server is runnable.


Use cases

1. Local development β€” fast edit/test loop

Run straight from TypeScript source, no build step, using tsx:

npm run dev

Or compile and run the built output (what the hosts actually execute):

npm run build   # tsc -> dist/
npm start       # node dist/index.js

Rebuild after editing src/ if you're testing through a host, since hosts run the compiled dist/index.js.

Tip: for a zero-build inner loop, you can point .mcp.json at "command": "npx", "args": ["tsx", "src/index.ts"] so the host runs the TypeScript directly. Switch back to dist/index.js for a "production" run.

2. Run the HTTP server + demo client

The same tool, served over Streamable HTTP instead of stdio. Start the server (it runs on its own; a host connects by URL), then run the client in a second terminal:

npm run start:http     # -> http://localhost:3000/mcp   (or npm run dev:http)
npm run client         # connects, lists tools, calls get_current_time

The server is stateless (no Mcp-Session-Id): each POST /mcp builds a fresh server + transport. See docs/transports.md for why.

By default it replies over SSE so it can stream progress notifications (see the stream_load tool). Set MCP_JSON=1 for single plain-JSON replies (easier for curl, but no streaming):

MCP_JSON=1 npm run start:http

curl -s -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

To watch the raw SSE stream (progress events, then the final result), run the server in default SSE mode and see the streaming example in docs/transports.md.

To connect Claude Code to the HTTP server instead of stdio:

claude mcp add --transport http my-http-server http://localhost:3000/mcp

3. Inspect & debug with the MCP Inspector

The official Inspector is a standalone host with a UI β€” the fastest way to see your tools, call them, and read stderr logs without involving Claude:

npx @modelcontextprotocol/inspector node dist/index.js

It opens a local web UI where you can run the initialize handshake, browse tools/list, and invoke get_current_time with arguments.

4. Test the raw protocol by hand

This is exactly what a host does β€” feed it the three startup messages and a call:

printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_current_time","arguments":{"timezone":"Asia/Kolkata"}}}' \
| node dist/index.js 2>/dev/null

5. Connect to Claude Code

.mcp.json is committed, so Claude Code picks the server up automatically:

cd <this-project>
claude                 # start a session in the project dir

On first launch, approve the my-first-server prompt (remembered per project). Then inside the session:

/mcp                   # verify: my-first-server -> connected, 1 tool
what time is it in Tokyo right now?   # triggers get_current_time

Debug connection issues with:

claude --debug         # shows the handshake and the server's stderr logs

6. Connect to the Claude desktop app

The desktop app's config is machine-local (not in this repo). On macOS it lives at:

~/Library/Application Support/Claude/claude_desktop_config.json

Add an entry under mcpServers pointing at this machine's absolute path to dist/index.js:

{
  "mcpServers": {
    "my-first-server": {
      "command": "node",
      "args": ["/absolute/path/to/this/repo/dist/index.js"]
    }
  }
}

Then fully quit (Cmd+Q) and reopen the app β€” config is only read at startup. Look for the tools icon and ask the same time question.


After you push to GitHub

Committed: src/, package.json, package-lock.json, tsconfig.json, .mcp.json, README.md, .gitignore.

Not committed (regenerated per machine): node_modules/, dist/, and the Claude desktop config (it's outside the repo).

So a teammate cloning the repo just runs:

git clone <repo> && cd <repo>
npm install            # builds dist/ automatically
claude                 # Claude Code auto-detects .mcp.json; approve once

The desktop app is the only piece they re-register locally (use case 6), because its config uses an absolute path unique to their machine.


Publishing to npm (@srav/mcp-time-server)

Publishing lets anyone add the server with a single npx line β€” no clone, no build, no absolute paths.

Prerequisites already in place:

  • bin maps my-mcp-server β†’ dist/index.js, and src/index.ts starts with #!/usr/bin/env node, so the published package is directly runnable.

  • files: ["dist"] ensures the compiled output ships in the tarball. (Without it, npm falls back to .gitignore β€” which excludes dist/ β€” and would publish an empty package.)

  • prepare builds dist/ automatically on npm publish.

  • publishConfig.access = "public" publishes the scoped package publicly.

Verify, then publish:

npm pack --dry-run      # confirm dist/index.js is in the tarball
npm login               # once
npm publish             # runs prepare (build) then uploads

After publishing, switch .mcp.json (or use claude mcp add) from the local path to the published package:

// local dev (current β€” keep while iterating)
{ "command": "node", "args": ["dist/index.js"] }

// published (after npm publish)
{ "command": "npx", "args": ["-y", "@srav/mcp-time-server"] }
# or register from the CLI, user scope = available in every project
claude mcp add --scope user my-first-server -- npx -y @srav/mcp-time-server

npx downloads the package to its cache on first run, then executes the bin entry β€” from there the stdio JSON-RPC flow is identical to running locally.

Distribution models at a glance

Model

.mcp.json command

When

Local path

node dist/index.js

You, developing on this machine

Cloned repo

node dist/index.js

Teammates who clone + npm install

Published npm

npx -y @srav/mcp-time-server

Anyone, anywhere β€” no clone needed

Available Tools

2 tools
get_current_timeGet current timeA

Returns the current date and time. Optionally pass an IANA timezone (e.g. 'Asia/Kolkata', 'America/New_York') to get the time there.

ParametersJSON Schema
NameRequiredDescriptionDefault
timezoneNoIANA timezone name, e.g. 'Asia/Kolkata'. Defaults to the server's local time.

Output Schema

ParametersJSON Schema
NameRequiredDescription
isoYesISO 8601 timestamp
timezoneYesThe IANA timezone the result is expressed in
unixSecondsYesSeconds since the Unix epoch

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the burden of behavioral disclosure. It conveys the core read-only behavior and optional timezone behavior, but does not mention edge cases like invalid timezone handling or output format. The output schema presumably covers return structure, but some informational context is still missing.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence: the main purpose comes first, and the optional parameter detail follows. Every word earns its place, with no filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one optional parameter and an output schema, the description provides the essential information to invoke it correctly. It covers the default behavior and the optional timezone usage. Error behavior for invalid timezone is not described, but that is a minor gap given the tool's simplicity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents the timezone parameter. The description adds helpful IANA examples and clarifies the optionality and purpose of the parameter, but it does not provide substantial meaning beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the operation ('Returns the current date and time') and the resource, making the tool's purpose unmistakable. It does not explicitly differentiate from sibling stream_load, but the purpose is precise enough that no confusion is likely.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies when to use the tool: whenever the current date/time is needed. It also explains the optional timezone parameter. However, it does not explicitly state exclusions or compare against alternatives beyond that implied context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

stream_loadStreaming load demoA

Simulates loading in steps, streaming one progress notification per step, then returns a final message.

ParametersJSON Schema
NameRequiredDescriptionDefault
stepsNoNumber of progress steps to emit (default 5).

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses that the tool only simulates loading, streams one notification per step, and ends with a final message. It does not discuss side effects directly, but 'simulates' strongly implies no real-world impact.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single concise sentence that front-loads the main action and then explains the streaming behavior and final result. Every word earns its place, and there is no redundant repetition of the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter demo tool, the description plus schema is nearly sufficient: steps is fully documented and the simulation/streaming behavior is clear. However, with no output schema, the exact content of the final message and the shape of progress notifications remain unspecified.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already documents steps with its type, range, and default value, giving 100% schema coverage. The description adds no extra parameter-level meaning, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Simulates') and names the resource ('loading in steps') along with the exact behavior: streaming one progress notification per step and returning a final message. This clearly distinguishes it from the unrelated sibling get_current_time.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The word 'Simulates' and the title 'Streaming load demo' imply this is for demonstration or testing rather than real loading, but the description never explicitly states when to use it or when to avoid it. No alternatives or exclusions are mentioned.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv1.0.0
    • First observedget_current_time
    • First observedstream_load

TDQS

A3.7/5.0

Scored across 2 tools

Disambiguation5/5

The two tools are completely unrelated in functionβ€”one returns time and the other simulates streamingβ€”so there is zero ambiguity about which to choose. No overlap or boundary confusion exists.

Naming Consistency5/5

Both tool names follow a clear snake_case verb_noun pattern: get_current_time and stream_load. The naming is predictable and consistent, even if the verbs are not from the same domain.

Tool Count3/5

With only two tools, the set feels thin and barely qualifies as a server. It's on the borderline of the acceptable range for a minimal utility or demo server.

Completeness2/5

The tools do not belong to a shared domain, so there is no meaningful coverage to assess. Each tool is an isolated snippet with no related operations, leaving the server's purpose unclear and incomplete.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A hosted MCP server providing current time, date, and timezone conversion tools specifically designed for Claude Code remote sessions. It enables users to fetch localized time data and convert times between IANA timezones through a centralized API.
    -
  • A
    license
    D
    quality
    C
    maintenance
    A lightweight MCP server that provides date and time tools, including the ability to retrieve current timestamps and parse date strings with IANA timezone support. It enables AI models to interact with the host OS clock and perform temporal calculations via stdio transport.
    3
    8 npm
    7
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A remote MCP server exposing date/time tools over Streamable HTTP, enabling retrieval of current time for IANA timezones and listing available timezones, designed for use with web clients like Claude.ai.
    MIT