631 lines
No EOL
15 KiB
Text
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> |