docs/docs/first-server/typescript.mdx
2024-11-19 22:00:50 -05:00

631 lines
No EOL
15 KiB
Text

---
title: "TypeScript"
description: "Create a simple MCP server in TypeScript in 15 minutes"
---
Let's build your first MCP server in TypeScript! We'll create a weather server that provides current weather data as a resource and lets Claude fetch forecasts using tools.
<Note>
This guide uses the OpenWeatherMap API. You'll need a free API key from [OpenWeatherMap](https://openweathermap.org/api) to follow along.
</Note>
## Prerequisites
<Steps>
<Step title="Install Node.js">
You'll need Node.js 18 or higher:
```bash
node --version # Should be v18 or higher
npm --version
```
</Step>
<Step title="Create a new project">
```bash
mkdir weather-server
cd weather-server
npm init -y
```
</Step>
<Step title="Install dependencies">
```bash
npm install @modelcontextprotocol/sdk axios dotenv typescript @types/node
npm install --save-dev ts-node
```
</Step>
<Step title="Configure TypeScript">
Create `tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"include": ["src/**/*"]
}
```
</Step>
<Step title="Set up environment">
Create `.env`:
```bash
OPENWEATHER_API_KEY=your-api-key-here
```
</Step>
</Steps>
## Create your server
<Steps>
<Step title="Create project structure">
```bash
mkdir src
touch src/index.ts
touch src/types.ts
```
</Step>
<Step title="Define types">
In `src/types.ts`:
```typescript
export interface OpenWeatherResponse {
main: {
temp: number;
humidity: number;
};
weather: Array<{
description: string;
}>;
wind: {
speed: number;
};
dt_txt?: string;
}
export interface WeatherData {
temperature: number;
conditions: string;
humidity: number;
wind_speed: number;
timestamp: string;
}
export interface ForecastDay {
date: string;
temperature: number;
conditions: string;
}
export interface GetForecastArgs {
city: string;
days?: number;
}
// Type guard for forecast arguments
export function isValidForecastArgs(args: unknown): args is GetForecastArgs {
return (
typeof args === "object" &&
args !== null &&
"city" in args &&
typeof args.city === "string" &&
(args.days === undefined || typeof args.days === "number")
);
}
```
</Step>
<Step title="Add the base code">
In `src/index.ts`:
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
ErrorCode,
McpError
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import dotenv from "dotenv";
import {
WeatherData,
ForecastDay,
GetForecastArgs,
OpenWeatherResponse,
isValidForecastArgs
} from "./types.js";
dotenv.config();
const API_KEY = process.env.OPENWEATHER_API_KEY;
if (!API_KEY) {
throw new Error("OPENWEATHER_API_KEY environment variable is required");
}
const API_CONFIG = {
BASE_URL: 'http://api.openweathermap.org/data/2.5',
DEFAULT_CITY: 'San Francisco',
ENDPOINTS: {
CURRENT: 'weather',
FORECAST: 'forecast'
}
} as const;
class WeatherServer {
private server: Server;
private axiosInstance;
constructor() {
this.server = new Server({
name: "example-weather-server",
version: "0.1.0"
});
// Configure axios with defaults
this.axiosInstance = axios.create({
baseURL: API_CONFIG.BASE_URL,
params: {
appid: API_KEY,
units: "metric"
}
});
this.setupHandlers();
this.setupErrorHandling();
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupHandlers(): void {
this.setupResourceHandlers();
this.setupToolHandlers();
}
private setupResourceHandlers(): void {
// Implementation continues in next section
}
private setupToolHandlers(): void {
// Implementation continues in next section
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.log("Weather MCP server running on stdio");
}
}
const server = new WeatherServer();
server.run().catch(console.error);
```
</Step>
<Step title="Add resource handlers">
Add this to the `setupHandlers` method:
```typescript
private setupResourceHandlers(): void {
this.server.setRequestHandler(
ListResourcesRequestSchema,
async () => ({
resources: [{
uri: `weather://${API_CONFIG.DEFAULT_CITY}/current`,
name: `Current weather in ${API_CONFIG.DEFAULT_CITY}`,
mimeType: "application/json",
description: "Real-time weather data including temperature, conditions, humidity, and wind speed"
}]
})
);
this.server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
const city = API_CONFIG.DEFAULT_CITY;
if (request.params.uri !== `weather://${city}/current`) {
throw new McpError(
ErrorCode.InvalidRequest,
`Unknown resource: ${request.params.uri}`
);
}
try {
const response = await this.axiosInstance.get<OpenWeatherResponse>(
API_CONFIG.ENDPOINTS.CURRENT,
{
params: { q: city }
}
);
const weatherData: WeatherData = {
temperature: response.data.main.temp,
conditions: response.data.weather[0].description,
humidity: response.data.main.humidity,
wind_speed: response.data.wind.speed,
timestamp: new Date().toISOString()
};
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(weatherData, null, 2)
}]
};
} catch (error) {
if (axios.isAxiosError(error)) {
throw new McpError(
ErrorCode.InternalError,
`Weather API error: ${error.response?.data.message ?? error.message}`
);
}
throw error;
}
}
);
}
```
</Step>
<Step title="Add tool handlers">
Add these handlers to the `setupHandlers` method:
```typescript
private setupToolHandlers(): void {
this.server.setRequestHandler(
ListToolsRequestSchema,
async () => ({
tools: [{
name: "get_forecast",
description: "Get weather forecast for a city",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "City name"
},
days: {
type: "number",
description: "Number of days (1-5)",
minimum: 1,
maximum: 5
}
},
required: ["city"]
}
}]
})
);
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
if (request.params.name !== "get_forecast") {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
if (!isValidForecastArgs(request.params.arguments)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid forecast arguments"
);
}
const city = request.params.arguments.city;
const days = Math.min(request.params.arguments.days || 3, 5);
try {
const response = await this.axiosInstance.get<{
list: OpenWeatherResponse[]
}>(API_CONFIG.ENDPOINTS.FORECAST, {
params: {
q: city,
cnt: days * 8 // API returns 3-hour intervals
}
});
const forecasts: ForecastDay[] = [];
for (let i = 0; i < response.data.list.length; i += 8) {
const dayData = response.data.list[i];
forecasts.push({
date: dayData.dt_txt?.split(' ')[0] ?? new Date().toISOString().split('T')[0],
temperature: dayData.main.temp,
conditions: dayData.weather[0].description
});
}
return { toolResult: forecasts };
} catch (error) {
if (axios.isAxiosError(error)) {
throw new McpError(
ErrorCode.InternalError,
`Weather API error: ${error.response?.data.message ?? error.message}`
);
}
throw error;
}
}
);
}
```
</Step>
<Step title="Build and test">
Add these scripts to `package.json`:
```json
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
}
}
```
Then run:
```bash
npm run build
npm install -g .
```
</Step>
</Steps>
## Connect to Claude Desktop
<Steps>
<Step title="Update Claude config">
Add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"weather": {
"command": "weather-server",
"env": {
"OPENWEATHER_API_KEY": "your-api-key",
}
}
}
}
```
</Step>
<Step title="Restart Claude">
1. Quit Claude completely
2. Start Claude again
3. Look for your weather server in the 🔌 menu
</Step>
</Steps>
## Try it out!
<AccordionGroup>
<Accordion title="Check Current Weather" active>
Ask Claude:
```
What's the current weather in San Francisco? Can you analyze the conditions?
```
</Accordion>
<Accordion title="Get a Forecast">
Ask Claude:
```
Can you get me a 5-day forecast for Tokyo and tell me if I should pack an umbrella?
```
</Accordion>
<Accordion title="Compare Weather">
Ask Claude:
```
Can you analyze the forecast for both Tokyo and San Francisco and tell me which city will be warmer this week?
```
</Accordion>
</AccordionGroup>
## Understanding the code
<Tabs>
<Tab title="Type Safety">
```typescript
interface WeatherData {
temperature: number;
conditions: string;
humidity: number;
wind_speed: number;
timestamp: string;
}
```
TypeScript adds type safety to our MCP server, making it more reliable and easier to maintain.
</Tab>
<Tab title="Resources">
```typescript
this.server.setRequestHandler(
ListResourcesRequestSchema,
async () => ({
resources: [{
uri: `weather://${DEFAULT_CITY}/current`,
name: `Current weather in ${DEFAULT_CITY}`,
mimeType: "application/json"
}]
})
);
```
Resources provide data that Claude can access as context.
</Tab>
<Tab title="Tools">
```typescript
{
name: "get_forecast",
description: "Get weather forecast for a city",
inputSchema: {
type: "object",
properties: {
city: { type: "string" },
days: { type: "number" }
}
}
}
```
Tools let Claude take actions through your server with type-safe inputs.
</Tab>
</Tabs>
## Best practices
<CardGroup cols={1}>
<Card
title="Error Handling"
icon="shield"
>
```typescript
try {
const response = await this.axiosInstance.get(...);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new McpError(
ErrorCode.InternalError,
`API error: ${error.response?.data.message}`
);
}
throw error;
}
```
</Card>
<Card
title="Type Guards"
icon="check"
>
```typescript
function isValidForecastArgs(args: unknown): args is GetForecastArgs {
return (
typeof args === "object" &&
args !== null &&
"city" in args &&
typeof args.city === "string"
);
}
```
</Card>
<Card
title="Environment Variables"
icon="gear"
>
```typescript
if (!process.env.OPENWEATHER_API_KEY) {
throw new Error("OPENWEATHER_API_KEY is required");
}
```
</Card>
<Card
title="Constants"
icon="box"
>
```typescript
const API_CONFIG = {
BASE_URL: '...',
ENDPOINTS: {
CURRENT: 'weather',
FORECAST: 'forecast'
}
} as const;
```
</Card>
</CardGroup>
## Available Transports
While this guide uses stdio transport, MCP supports multiple transport options:
### WebSocket
```typescript
import { WebSocketServerTransport } from "@modelcontextprotocol/sdk/server/websocket.js";
const transport = new WebSocketServerTransport();
```
### SSE (Server-Sent Events)
```typescript
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
const transport = new SSEServerTransport("/events", response);
```
## Troubleshooting
### Build Errors
```bash
# Check TypeScript version
npx tsc --version
# Clean and rebuild
rm -rf dist/
npm run build
```
### Runtime Errors
Look for detailed error messages in the Claude app logs:
```bash
# Launch Claude with logging
open /Applications/Claude.app --stdout ~/Claude.log --stderr ~/Claude.log
# Monitor logs
tail -f ~/Claude.log
```
### Type Errors
```bash
# Check types without building
npx tsc --noEmit
```
## Next steps
<CardGroup cols={2}>
<Card
title="Architecture overview"
icon="sitemap"
href="/docs/concepts/architecture"
>
Learn more about the MCP architecture
</Card>
<Card
title="TypeScript SDK"
icon="node-js"
href="/api-reference/typescript/class-reference"
>
Read more about the TypeScript SDK
</Card>
</CardGroup>
<Note>
Need help? Ask Claude! Since it has access to the MCP SDK documentation, it can help you debug issues and suggest improvements to your server.
</Note>