Build MCP Server step by step

Written by
Audrey Miles
Updated on:July-14th-2025
Recommendation

In-depth exploration of the entire process of building an MCP server with TypeScript.

Core content:
1. Detailed steps to create an MCP server
2. Use the OpenWeatherMap API to obtain weather data
3. Building a TypeScript development environment and generating server scaffolding

Yang Fangxian
Founder of 53AI/Most Valuable Expert of Tencent Cloud (TVP)

In this article, we will demonstrate how to write an MCP server through a TypeScript MCP server example. We will create a weather server that provides current weather data as a resource and let Claude use tools to get the weather forecast.

Here we need to use the OpenWeatherMap API [2] to obtain weather data. Simply register and obtain a free API key on the API keys [3] page.

Environment Preparation

We need to prepare a TypeScript development environment, so we need to install Node.js and NPM.

# Check Node.js version, v18 or higher is required
node --version

# Check npm version
npm --version

Next we can directly use@modelcontextprotocol/create-serverThis tool creates a scaffolding MCP server:

$ npx @modelcontextprotocol/create-server weather-server
Need to install the following packages:
@modelcontextprotocol/create-server@0.3.1
Ok to proceed? (y) y

? What is the name of your MCP server? y
? What is the description of your server? A Model Context Protocol server
? Would you like to install this server  for  Claude.app? Yes
✔MCP server created successfully!
✓ Successfully added MCP server to Claude.app configuration

Next steps:
  cd  weather-server
  npm install
  npm run build   # or: npm run watch
  npm link        # optional, to make available globally

cd  weather-server

Then install the dependencies:

npm install --save axios dotenv

Next we need to set the environment variables, create.envdocument:

OPENWEATHER_API_KEY=your-api-key-here

Make sure.envAdd files to.gitignorein the file.

After the project is created, we can see that the project structure is as follows:

Template analysis

The project we created using the scaffolding creates a MCP server template for us by default. This template implements a simple note system that illustrates core MCP concepts such as resources and tools in the following ways:

  • Listing notes as resources
  • Read personal notes
  • Create a new note via the tool
  • Summarize all notes with prompts

Before we write our own MCP server, we can first learn how this note system is implemented, and then we can implement our own MCP server based on this template.

First, let's look at the imported dependency packages:

import  { Server }  from  "@modelcontextprotocol/sdk/server/index.js" ;
import  { StdioServerTransport }  from  "@modelcontextprotocol/sdk/server/stdio.js" ;
import  {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
from  "@modelcontextprotocol/sdk/types.js" ;

The first line of code is actually imported from the MCP SDKServerThis object represents an MCP server that will automatically respond to the initialization process initiated by the client.ListResourcesRequestSchema,ReadResourceRequestSchemaThese objects represent the request types defined in the MCP protocol.

For example, in the templateServerCreated an MCP server:

const  server =  new  Server(
  {
    name:  "weather-server" ,
    version:  "0.1.0" ,
  },
  {
    capabilities:
      resources: {},
      tools: {},
      prompts: {},
    },
  }
);

This code creates an MCP server with resources (for listing/reading notes), tools (for creating new notes), and prompts (for summarizing notes), and specifies the server name and version.

Once the server is initialized, we canServerObjectsetRequestHandlerMethod to register the processor. When the protocol object receives a request of the specified method, it will be called. This process is actually equivalent to writing an HTTP server. When a request of the specified HTTP method is received, the processor we registered will be called.

In the template, you can see several processors registered:

Resources

Here we need toResourcesResources are a concept in the MCP protocol, which means an object that can be read and operated. We can expose the data and content in the server to LLM as resources, and then LLM can operate these resources through tools. For example, we can use a note, a file, a database table, etc. as resources.

⚠️ It is important to note that resources are designed to be application controlled, which means that the client application can decide how and when to use them. Different MCP clients may handle resources in different ways. For example:

  • Claude Desktop now requires the user to explicitly select a resource before using it
  • Other clients may automatically select resources
  • Some implementations may even allow the AI ​​model itself to determine which resources to use

So server developers should be prepared to handle any of these interaction patterns when implementing resource support. To automatically expose data to the model, server authors should use model-controlled primitives, such as tools.

Resources represent any type of data that the MCP server wishes to provide to the client, and can include the following types:

  • File Contents
  • Database records
  • API Response
  • Real-time system data
  • Screenshots and Images
  • Log files
  • And much more

Each resource has a uniqueURIIdentifies and can contain text or binary data.

In the template, all note resources are exposed to the client as resources.setRequestHandlerRegistered handler to handle client'sresources/listask:

/**
 * Handler for listing available notes as resources.
 * Each note is exposed as a resource with the following characteristics:
 * - note:// URI scheme
 * - MIME type
 * - Human-readable name and description (including the note title)
 */

server.setRequestHandler(ListResourcesRequestSchema,  async  () => {
  return  {
    resources:  Object .entries(notes).map( ( [id, note] ) =>  ({
      uri:  `note:/// ${id} ` ,
      mimeType:  "text/plain" ,
      name: note.title,
      description:  `A text note:  ${note.title} ` ,
    })),
  };
});

Resource URI

Resources are identified using URIs that follow the following format:


For example:

  • file:///home/user/documents/report.pdf
  • postgres://database/customers/schema
  • screen://localhost/display1

inprotocolProtocol andpathThe path structure is defined by the MCP server implementation, although servers may define their own custom URI schemes.

Resource Type

Resources can contain two types of content:

Text resources

Text resources contain UTF-8 encoded text data, which are suitable for:

  • source code
  • Configuration Files
  • Log files
  • JSON/XML data
  • Plain text

Binary resources

The binary resource containsbase64Encoded raw binary data, these are suitable for:

  • picture
  • PDF File
  • Audio Files
  • Video Files
  • Other non-text formats

Resource Discovery

There are two main ways that clients can discover available resources:

Direct resources

Server viaresources/listThe endpoint exposes a list of specific resources. Each resource includes:

{
  uri: string;            // Unique identifier of the resource
  name: string;           // human readable name
  description?: string;   // optional description
  mimeType?: string;      // Optional MIME type
}

Resource Templates

For dynamic resources, the server can expose URI templates that clients can use to construct valid resource URIs:

{
  uriTemplate: string;    // URI template following RFC 6570
  name: string;           // human-readable name of the type
  description?: string;   // optional description
  mimeType?: string;      // Optional MIME type for all matching resources
}

Reading Resources

To read a resource, the client issues aresources/readRequest. The server responds with a resource content list like this:

{
  contents: [
    {
      uri: string;         // URI of the resource
      mimeType?: string;   // Optional MIME type
      // One of:
      text?: string;       // For text resources
      blob?: string;       // for binary resources (base64 encoded)
    }
  ]
}

The server may return multiple resources in response to aresources/readRequests, such as returning a list of files in a directory when reading a directory.

For example, the processor for reading notes in the template is implemented as follows:

/**
 * A handler for reading the contents of a specified note.
 * Accepts a note:// URI and returns the note contents as plain text.
 */

server.setRequestHandler(ReadResourceRequestSchema,  async  (request) => {
  const  url =  new  URL(request.params.uri);
  const  id = url.pathname.replace( /^\//"" );
  const  note = notes[id];

  if  (!note) {
    throw  new  Error ( `Note  ${id}  not found` );
  }

  return  {
    contents: [
      {
        uri: request.params.uri,
        mimeType:  "text/plain" ,
        text: note.content,
      },
    ],
  };
});

Resource Update

MCP supports real-time updates of resources through two mechanisms:

List changes

When the list of available resources changes, the server cannotification/resources/list_changedNotify the client.

Content Changes

Clients can subscribe to updates for a specific resource:

  • The client sends the resource URIresources/subscribeask
  • Sent by the server when a resource changesnotification/resources/updatenotify
  • The client canresources/readTo get the latest content
  • Clients can unsubscribe from resources/unsubscribe

Tools

Tools enable LLMs to perform actions through your server. Tools enable servers to expose executable functions to clients. Through tools, LLMs can interact with external systems, perform calculations, and perform actions in the real world. Tools are exposed from servers to clients so that AI models can automatically call them (grant approval).

The tools in MCP allow the server to expose executable functions that can be called by clients and used by LLM to perform operations. The implementation tools mainly include the following aspects:

  • Discovery : The client cantools/listEndpoint lists available tool calls
  • Call : Usetools/callEndpoint invocation facility where the server performs the requested operation and returns the result
  • Flexibility : Tools can range from simple calculations to complex API interactions

Like resources, tools are identified by a unique name and can include a description to identify their purpose, but unlike resources, tools represent dynamic operations that can modify state or interact with external systems.

Each tool is defined with the following structure:

{
  name: string;           // unique identifier of the tool
  description?: string;   // human readable description
  inputSchema: {  // JSON Schema for tool parameters
    type:  "object" ;
    properties: { ... }  // Tool specific parameters
  }
}

For example, in the template code,setRequestHandlerThe tool list processor is registered, the code is as follows:

/**
 * Handler for listing available tools.
 * Expose a "create_note" tool to let clients create new notes.
 */

server.setRequestHandler(ListToolsRequestSchema,  async  () => {
  return  {
    tools: [
      {
        name:  "create_note" ,
        description:  "Create a new note" ,
        inputSchema: {
          type"object" ,
          properties:
            title:
              type"string" ,
              description:  "Title of the note" ,
            },
            content: {
              type"string" ,
              description:  "Text content of the note" ,
            },
          },
          required: [ "title""content" ],
        },
      },
    ],
  };
});

The above code defines a tool structure namedcreate_noteThis tool is used to create a new note and needs to receive two parameters:titleandcontent, that is, the title and content of the note, so that the client knows that there iscreate_noteThis tool can be called, and it is known that calling the tool requires passing intitleandcontentparameter.

The above register only lists all available tools. To actually call the implementation tool, you also need to register the tool call processor. The code is as follows:

/**
 * Handler for creating new notes.
 * Creates a new note with the provided title and content and returns a success message.
 */

server.setRequestHandler(CallToolRequestSchema,  async  (request) => {
  switch  (request.params.name) {
    case  "create_note" : {
      const  title =  String (request.params.arguments?.title);
      const  content =  String (request.params.arguments?.content);
      if  (!title || !content) {
        throw  new  Error ( "Title and content are required" );
      }

      const  id =  String ( Object .keys(notes).length +  1 );
      notes[id] = { title, content };

      return  {
        content: [
          {
            type"text" ,
            text:  `Created note  ${id}${title} ` ,
          },
        ],
      };
    }

    default :
      throw  new  Error ( "Unknown tool" );
  }
});

The above code is very simple to implement, just according to the tool namecreate_noteTo create a new note and return a success message, when the client callscreate_noteWhen the tool is used, the above processor code will be triggered.

Prompts

PromptsHints are the mechanism in the MCP protocol for defining reusable hint templates and workflows that clients can easily expose to users and LLMs. Hints are designed to be user-controlled, meaning they are exposed from the server to the client so that users can explicitly choose to use them.

Prompt words in MCP are predefined templates that can:

  • Accepts dynamic parameters
  • Include context from resources
  • Chain multiple interactions
  • Guide specific workflows
  • As UI elements (such as slash commands)

Each Prompt definition contains the following structure:

{
  name: string;               // unique identifier of the prompt word
  description?: string;       // human readable description
  arguments?: [               // Optional parameter list
    {
      name: string;           // parameter identifier
      description?: string;   // description of the parameter
      required?: boolean;     // Required?
    }
  ]
}

The client canprompts/listendpoint to discover all available prompt words, such as making a request like the following:

// Request
{
  method:  "prompts/list"
}

// Response
{
  prompts: [
    {
      name:  "analyze-code" ,
      description:  "Analyze code for potential improvements" ,
      arguments: [
        {
          name:  "language" ,
          description:  "Programming language" ,
          required:  true
        }
      ]
    }
  ]
}

Then you can passprompts/getEndpoint to get detailed information of a specified prompt word:

// Request
{
  "method""prompts/get" ,
  "params" : {
    "name""analyze-code" ,
    "arguments" : {
      "language""python"
    }
  }
}

// Response
{
  "description""Analyze Python code for potential improvements" ,
  "messages" : [
    {
      "role""user" ,
      "content" : {
        "type""text" ,
        "text""Please analyze the following Python code for potential improvements:\n\n```python\ndef calculate_sum(numbers):\n total = 0\n for num in numbers:\n total = total + num\n return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n```"
      }
    }
  ]
}

For example, in the templatesetRequestHandlerThe prompt word list processor is registered, and the code is as follows:

/**
 * A handler for listing available cue words.
 * Expose a "summarize_notes" prompt for summarizing all notes.
 */

server.setRequestHandler(ListPromptsRequestSchema,  async  () => {
  return  {
    prompts: [
      {
        name:  "summarize_notes" ,
        description:  "Summarize all notes" ,
      },
    ],
  };
});

You can see that a file calledsummarize_notesThis prompt word is used to summarize all notes, but this prompt word does not define any parameters, so the client does not need to pass in any parameters when calling this prompt word.

Then to get the detailed information of the prompt word, you can useprompts/getThe endpoint is used to obtain the information. The template also usessetRequestHandlerThe prompt word acquisition processor is registered, and the code is as follows:

/**
 * A prompt word processor to summarize all notes.
 * Returns a prompt word, requests a summary of all notes, and embeds the note contents as a resource.
 */

server.setRequestHandler(GetPromptRequestSchema,  async  (request) => {
  if  (request.params.name !==  "summarize_notes" ) {
    throw  new  Error ( "Unknown prompt" );
  }

  const  embeddedNotes =  Object .entries(notes).map( ( [id, note] ) =>  ({
    type"resource"  as  const ,
    resource:
      uri:  `note:/// ${id} ` ,
      mimeType:  "text/plain" ,
      text: note.content,
    },
  }));

  return  {
    messages: [
      {
        role:  "user" ,
        content: {
          type"text" ,
          text:  "Please summarize the following notes:" ,
        },
      },
      ...embeddedNotes.map( ( note ) =>  ({
        role:  "user"  as  const ,
        content: note,
      })),
      {
        role:  "user" ,
        content: {
          type"text" ,
          text:  "Provide a concise summary of all the notes above." ,
        },
      },
    ],
  };
});

As you can see from the above code, when the prompt word is generated, all the note content will be embedded in the prompt word, so that the context has the relevant content of the note.

Start the server

At this point, we have implemented a simple MCP server and registered resource, tool, prompt word and other processors. Of course, we still need to start the server in the end so that the server we wrote can actually run.

In the templatestdioThe transmission starts the server, the code is as follows:

/**
 * Start the server using the stdio transport.
 * Allow the server to communicate via standard input/output streams.
 */

async  function  main ( {
  const  transport =  new  StdioServerTransport();
  await  server.connect(transport);
}

main().catch( ( error ) =>  {
  console .error( "Server error:" , error);
  process.exit( 1 );
});

stdioThe transport supports communication via standard input and output streams, which is particularly useful for local integration and command-line tools. We can use it in the following situationsstdio:

  • Building command line tools
  • Implement native integration
  • Need simple process communication
  • Using shell scripts

Apart fromstdiotransport, MCP also supports Server-Sent Events (SSE) transport, SSE transport supports server-to-client streaming via HTTP POST requests for client-to-server communication. We can use it in the following situationsSSE:

  • Only server-to-client streaming is required
  • Using restricted networks
  • Implementing simple updates

Writing Code

We have analyzed the implementation of the MCP server above. Next, we can write code according to our needs. Our requirement is to provide a weather query service. Here we can use weather data as a resource and expose a weather query tool.

First, we define the type of weather resource. The code is as follows:

// src/types/weather.ts
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 function to check the GetForecastArgs type
export  function  isValidForecastArgs ( args:  any ):  args  is  GetForecastArgs  {
  return  (
    typeof  args ===  "object"  &&
    args !==  null  &&
    "city"  in  args &&
    typeof  args.city ===  "string"  &&
    (args.days ===  undefined  ||  typeof  args.days ===  "number" )
  );
}

The type definitions here are mainly defined based on the response data types of the OpenWeather API, so that we can use these types conveniently.

Then write the following basic code to replace the templatesrc/index.tsThe code in:

// src/index.ts
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,
  Forecast Day,
  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:  "weather-server" ,
        version:  "0.1.0" ,
      },
      {
        capabilities:
          resources: {},
          tools: {},
        },
      }
    );

    // Configure the axios instance
    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  {
    //  TODO:  Implement resource handler
  }

  private  setupToolHandlers():  void  {
    //  TODO:  Implement tool handler
  }

  async  run():  Promise < void > {
    const  transport =  new  StdioServerTransport();
    await  this .server.connect(transport);

    console .error( "Weather MCP server running on stdio" );
  }
}

const  server =  new  WeatherServer();
server.run().catch( console .error);

Here we have done a little encapsulation based on the template and defined it in a class way. We mainly do the following things:

  • Defines the type of weather resource
  • Initialized an MCP server instance
  • Registered resource and tool handlers
  • Started the server

Among them, the resource and tool processors we passTODONow that we have marked them, we can implement these processors.

Implementing a resource handler

existsetupResourceHandlersIn the method, we implement the resource processor. First, add a processor that lists resources, and then add a processor that reads resources. The code is as follows:

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,  null2 )
          }]
        };
      }  catch  (error) {
        if  (axios.isAxiosError(error)) {
          throw  new  McpError(
            ErrorCode.InternalError,
            `Weather API error:  ${error.response?.data.message ?? error.message} `
          );
        }
        throw  error;
      }
    }
  );
}

The implementation of the processor that lists resources is very simple. Here we customizeweatherThe protocol is JSON format.axiosRequest the OpenWeather API to get the current weather data, and then convert it intoWeatherDataType and return it.

Implementing a tool processor

After the resource processor is implemented, we can implement the tool processor. The tool processor is mainly used to implement some tool functions. Here we implement a tool to query the future weather forecast. The code is as follows:

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 ||  35 );

      try  {
        const  response =  await  this .axiosInstance.get<{
          list: OpenWeatherResponse[]
        }>(API_CONFIG.ENDPOINTS.FORECAST, {
          params: {
            q: city,
            cnt: days *  8  // The API returns data at 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  {
          content: [{
            type"text" ,
            text:  JSON .stringify(forecasts,  null2 )
          }]
        };
      }  catch  (error) {
        if  (axios.isAxiosError(error)) {
          return  {
            content: [{
              type"text" ,
              text:  `Weather API error:  ${error.response?.data.message ?? error.message} `
            }],
            isError:  true ,
          }
        }
        throw  error;
      }
    }
  );
}

Again, you need to implement the handler for listing tools first, and then implement the handler for calling tools. Here we only define a handler calledget_forecastThis tool is used to obtain the weather forecast for a specified city and needs to receive two parameterscityanddays,incityis the city name,daysThe default is 3 days. Of course, the data is obtained by requesting the OpenWeather API.

In fact, the resources we defined above can be directly obtained through tools. We can add a tool to obtain the current weather. Because the data is obtained through the OpenWeather API, it is also possible not to define resources. However, we define resources here to demonstrate the usage of MCP.

test

At this point we have implemented a simple weather MCP service, and then we can test it.

First we need to build the project:

npm run build

Then you need to update the configuration of Claude Desktop:

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

Add our weather service to the configuration as follows:

{
  "mcpServers" : {
    //...... Other server configurations
    "weather" : {
      "command""node" ,
      "args" : [ "/Users/cnych/src/weather-server/build/index.js" ],
      "env" : {
        "OPENWEATHER_API_KEY""your_openweather_api_key"
      }
    }
  }
}

inargsis the file path after we build.envThis is the key of the OpenWeather API that we need to configure. Once the configuration is complete, restart Claude Desktop.

test

Next we can test it. Click the number button in the lower right corner of the Claude Desktop input box, and the definedget_forecasttool.

Next we can test it, for example, we ask Claude for the weather forecast for the next 5 days:

Can you get me a 5-day forecast  for  Beijing and tell me  if  I should pack an umbrella?

You can see that it will callget_forecasttool (authorization required) and displays the results.

debug

If we encounter problems during testing, we can debug it in some ways, such as viewing the detailed log of MCP:

# View logs in real time
tail -n 20 -f ~/Library/Logs/Claude/mcp* .log

The logs here capture information such as server connection events, configuration issues, runtime errors, message exchanges, etc.

In addition to the log, we can alsoChrome DevToolsTo debug, access Chrome's developer tools in Claude Desktop to view client-side errors.~/Library/Application\ Support/Claude/developer_settings.jsonAdd the following configuration to open DevTools:

{
  "allowDevTools"true
}

Then use the shortcut keyCommand+Option+Shift+iYou can open DevTools, just like debugging in the Chrome browser.

In addition to the above conventional debugging methods, Claude MCP also provides aInspectorTool, MCP Inspector is an interactive developer tool for testing and debugging MCP servers.

Directly throughnpxThe command can be used without installation:

npx @modelcontextprotocol/ inspector <command>
# or
npx @modelcontextprotocol/inspector < command > <arg1> <arg2>

If the server package comes from NPM, you can start it as follows:

npx -y @modelcontextprotocol/inspector npx <package-name> <args>
# For example
npx -y @modelcontextprotocol/inspector npx server-postgres postgres://127.0.0.1/testdb

If it is a locally built package, you can start it in the following way:

npx @modelcontextprotocol/inspector node path/to/server/index.js args...

For example, the weather service we built above can be started in the following way:

npx @modelcontextprotocol/inspector node /Users/cnych/src/weather-server/build/index.js

InspectorAfter the tool is started,localhost:5173Start a web page on which we can test and debug our weather service.

Please note that we need to click on the rightEnvironment Variablesbutton, then addOPENWEATHER_API_KEYEnvironment variables, the value is the key of the OpenWeather API we applied for, and then clickConnectbutton to connect to the weather service.

After the connection is successful, we can see the resources and tools of the weather service in the main window on the right, and we can test and debug. ClickList Resourcesbutton to list the resources of the weather service, and click on the listed resources to read and display the resource content.

We can also test Tools by clickingList Toolsbutton to list the weather service tools, then click a specific tool, enter the parameters and clickRun Toolbutton to invoke the tool and display the results.

Of course, in addition to Resources and Tools, you can also test Prompts, Sampling, etc.

So far we have implemented a simple weather MCP service.