# 🔧 MCP Fundamentals: Tools, Resources, Prompts, and Capability Negotiation (Deep Dive)

---

*This is Part 2 of the AI Engineering with TypeScript series.*
*Prerequisites: Read [Part 1 — What is MCP?](https://blog.techtush.in) first.*
*MCP SDK: @modelcontextprotocol/sdk v1.x · Protocol version: 2025-06-18*
## 🗺️ What we'll cover
In Part 1 we built a simple task manager MCP server to understand the basics. Now we go deeper:
 * 🔧 **Tools** — complete API, annotations, progress reporting, error handling
 * 📄 **Resources** — static vs dynamic, URI templates, list/read lifecycle
 * 💬 **Prompts** — arguments, dynamic content, embedding resources
 * 🤝 **Capability negotiation** — exactly what happens during initialize
 * 📦 **Packaging** — bundle your server as an npm package and run it with npx
Let's build a **Weather + Forecast MCP server** throughout this post — realistic enough to demonstrate every concept. ☁️
## 🤝 Part 1: Capability Negotiation — what really happens on connect
Before your first tool call, client and server run a handshake. Capability negotiation runs on connect: the client tells the server which protocol version and features it supports, the server replies with its own capabilities — and from there, both sides know which message types are available. This is what lets MCP evolve without breaking older clients.
Here is the actual JSON-RPC exchange:
**→ Client sends: initialize**
```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "roots":    { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": {
      "name":    "claude-desktop",
      "version": "1.2.0"
    }
  }
}

```
**← Server responds: initialize result**
```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools":     { "listChanged": true },
      "resources": {},
      "prompts":   {}
    },
    "serverInfo": {
      "name":    "weather-server",
      "version": "1.0.0"
    }
  }
}

```
**→ Client sends: notifications/initialized (one-way, no response)**
```json
{ "jsonrpc": "2.0", "method": "notifications/initialized" }

```
Three things to notice here:
**protocolVersion is a date string**, not semver — like "2025-06-18". The protocol uses date-based versioning because the spec evolves in discrete snapshots. The client sends the version it wants to speak; the server can accept it or reject it.
**Capabilities are declared, not assumed.** If your server doesn't declare "tools": {}, clients won't call tools/list. If the server declares "tools": { "listChanged": true }, it's promising to send notifications/tools/list_changed whenever the tool list changes — the client will re-fetch on that notification.
**The SDK handles all of this for you.** McpServer automatically declares capabilities based on what you register. You never write initialize handlers manually. ✅
## 🔧 Part 2: Tools — the complete API
### The anatomy of a tool
A tool has four parts:
```typescript
server.registerTool(
  'tool_name',                 // 1️⃣ Name — unique identifier, snake_case
  {
    description: '...',        // 2️⃣ Description — what the LLM reads
    inputSchema: { ... },      // 3️⃣ Input schema — Zod schema for type safety
    annotations: { ... },      // 4️⃣ Annotations — hints about behavior (optional)
  },
  async (inputs, extra) => {   // 5️⃣ Handler — your business logic
    return { content: [...] }; // 6️⃣ Response — text, image, or resource content
  }
);

```
### Setting up the weather server
```typescript
// src/index.ts
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:    'weather-server',
  version: '1.0.0',
});

```
### Tool annotations — telling the LLM how to behave
Annotations are hints that help AI clients understand a tool's risk profile *before* calling it.
```typescript
server.registerTool(
  'get_current_weather',
  {
    description: 'Get current weather conditions for a city',
    inputSchema: {
      city:    z.string().describe('City name, e.g. "Pune" or "London"'),
      country: z.string().length(2).optional().describe('ISO country code'),
      units:   z.enum(['metric', 'imperial']).default('metric'),
    },
    annotations: {
      readOnlyHint:   true,  // 👈 this tool only reads data — no side effects
      idempotentHint: true,  // 👈 safe to call multiple times with same inputs
    },
  },
  async ({ city, country, units }) => {
    const location = country ? `${city}, ${country}` : city;
    const temp     = units === 'metric' ? '28°C' : '82°F';

    return {
      content: [{
        type: 'text',
        text: `🌤️ Current weather in ${location}:\n  Temperature: ${temp}\n  Condition: Partly Cloudy`,
      }],
    };
  }
);

```
The four standard annotation hints are:
| Annotation | Meaning |
|---|---|
| readOnlyHint: true | Only reads data, no side effects |
| idempotentHint: true | Calling twice with same args = same result |
| destructiveHint: true | May delete or modify data — use with caution |
| openWorldHint: true | Interacts with external services |
### Returning different content types
Tools can return text, image, or resource content:
```typescript
server.registerTool(
  'get_weather_map',
  {
    description: 'Get a weather radar map image for a region',
    inputSchema: {
      region: z.enum(['north', 'south', 'east', 'west']),
    },
    annotations: { readOnlyHint: true },
  },
  async ({ region }) => {
    const imageBase64 = await fetchRadarImage(region); // returns base64 string

    return {
      content: [
        {
          type: 'text',
          text: `🗺️ Weather radar for ${region} region:`,
        },
        {
          type: 'image',     // 👈 return an image!
          data: imageBase64,
          mimeType: 'image/png',
        },
      ],
    };
  }
);

```
### Proper error handling in tools
Return isError: true for expected errors — don't throw. Throwing is for unexpected exceptions.
```typescript
server.registerTool(
  'get_forecast',
  {
    description: 'Get a 5-day weather forecast for a city',
    inputSchema: { city: z.string().min(1) },
  },
  async ({ city }) => {
    const cityData = await lookupCity(city);
    if (!cityData) {
      return {
        content: [{
          type: 'text',
          text: `❌ City "${city}" not found.`,
        }],
        isError: true,  // 👈 tells the LLM this is an error, not a result
      };
    }
    
    return { content: [{ type: 'text', text: 'Forecast data here...' }] };
  }
);

```
### Progress notifications for long-running tools
```typescript
server.registerTool(
  'analyze_weather_patterns',
  {
    description: 'Analyse historical weather patterns for a city',
    inputSchema: { city: z.string(), years: z.number().int() },
  },
  async ({ city, years }, { reportProgress }) => {
    for (let i = 0; i < years; i++) {
      // 📊 Report progress
      await reportProgress({
        progress: i + 1,
        total:    years,
        message:  `Analysing year ${i + 1}...`,
      });
      await analyseYear(city, i); 
    }

    return {
      content: [{ type: 'text', text: `✅ Analysis complete` }],
    };
  }
);

```
## 📄 Part 3: Resources — exposing read-only data
Resources are read-only data sources identified by a URI. Think of them as GET endpoints — the AI reads them for context, but doesn't modify them.
### Static resources
A static resource has a fixed URI:
```typescript
server.registerResource(
  'weather-stations',                    
  'weather://stations/all',             
  {
    name:        'All Weather Stations',
    description: 'List of all monitored weather stations',
    mimeType:    'application/json',
  },
  async () => {
    const stations = await db.getStations();
    return {
      contents: [{
        uri:      'weather://stations/all',
        mimeType: 'application/json',
        text:     JSON.stringify(stations, null, 2),
      }],
    };
  }
);

```
### Dynamic resources with URI templates
URI templates let you expose a whole class of resources. Use {variable} placeholders:
```typescript
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';

server.registerResource(
  'city-weather-history',
  new ResourceTemplate('weather://history/{city}/{year}', { list: undefined }),
  {
    name:        'City Weather History',
    mimeType:    'application/json',
  },
  async (uri, { city, year }) => {           // 👈 variables extracted from URI
    const data = await fetchHistory(city, parseInt(year));
    return {
      contents: [{
        uri:      uri.href,
        mimeType: 'application/json',
        text:     JSON.stringify(data, null, 2),
      }],
    };
  }
);

```
### When to use resources vs tools
| Use a **Resource** when... | Use a **Tool** when... |
|---|---|
| The data is read-only | The operation has side effects |
| The URI uniquely identifies the data | Parameters determine the operation |
| It's like a GET endpoint | It's like a POST/PUT/DELETE endpoint |
## 💬 Part 4: Prompts — reusable templates with arguments
Prompts are reusable templates that help users talk to models in a consistent way.
### Embedding a resource inside a prompt
Prompts can pre-load resources as context:
```typescript
server.registerPrompt(
  'station-analysis',
  {
    name:        'Station Network Analysis',
    description: 'Analyse the health of all weather stations',
  },
  async () => ({
    messages: [{
      role:    'user',
      content: [
        {
          type: 'resource',          // 👈 embed a resource as context
          resource: {
            uri:      'weather://stations/all',
            mimeType: 'application/json',
            text:     JSON.stringify(await fetchAllStations()),
          },
        },
        {
          type: 'text',
          text: 'Based on the station data above, identify any offline stations.',
        },
      ],
    }],
  })
);

```
## 🔌 Part 5: Wiring it all together
Here's the complete src/index.ts with the stdio transport:
```typescript
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const server = new McpServer({
  name:    'weather-server',
  version: '1.0.0',
});

// --- Register Tools, Resources, Prompts here ---

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('☁️ Weather MCP Server running on stdio');
}

main().catch(console.error);

```
> ⚠️ **The shebang line #!/usr/bin/env node at the very top is required for npx to execute the file directly.**
> 
## 📦 Part 6: Packaging your MCP server as an npm package
Once you package your server, anyone can run it with a single command — no install instructions, just npx your-package-name. 🚀
### Step 1: Configure package.json for npx
The bin field allows the server to be run as a CLI command.
```json
{
  "name": "@yourname/weather-mcp",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "weather-mcp": "dist/index.js"
  },
  "files": [
    "dist",
    "README.md"
  ],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "zod": "^3.25.0"
  }
}

```
*(Note: Ensure your build pipeline preserves the shebang line in your compiled dist/index.js file!)*
### Step 2: Build and test locally
Before publishing, test the full npx flow locally using npm link:
```bash
# In your server project directory
npm run build
npm link

# Now test it — exactly what users will do
npx weather-mcp

# Unlink when done
npm unlink weather-mcp

```
### Step 3: Publish to npm 🚀
```bash
npm login
npm publish --access public

```
### Step 4: Use it in Claude Desktop with npx
Using npx -y downloads and runs the server if it's published to npm, making it easy for other developers to use your server without manual installation.
```json
{
  "mcpServers": {
    "weather": {
      "command": "npx",
      "args": ["-y", "@yourname/weather-mcp"],
      "env": {
        "WEATHER_API_KEY": "your-api-key-here"
      }
    }
  }
}

```
The -y flag skips the installation confirmation prompt. When Claude starts, it grabs the latest version automatically.
## 🎯 Summary
In this deep dive you learned:
 * 🤝 **Capability negotiation** — what actually happens during initialize
 * 🔧 **Tools** — annotations, content types, isError, progress reporting
 * 📄 **Resources** — static URIs, ResourceTemplate for dynamic URIs
 * 💬 **Prompts** — argument-driven prompts and embedding resources
 * 📦 **npx packaging** — bin field, shebang, and using with Claude Desktop
In **Part 3** we'll connect this server to an AI agent that plans, calls multiple tools in sequence, and reasons about the results. 🤖

---
 
## 📚 Further reading
 
- 📖 [MCP TypeScript SDK docs](https://ts.sdk.modelcontextprotocol.io/)
- 🔧 [Official SDK GitHub](https://github.com/modelcontextprotocol/typescript-sdk)
- 🧪 [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
- 📦 [MCP official server examples](https://github.com/modelcontextprotocol/servers)
- 🗂️ [MCP protocol spec](https://modelcontextprotocol.io/specification/latest)
- ⚡ [Part 1: What is MCP?](https://blog.techtush.in/what-is-mcp-model-context-protocol-a-typescript-developer-s-guide)

