Build an MCP server changeddocs/2026-07-28/develop/build-server
Upstream edited this page at 12 Oct 2026 16:26 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 12 Oct 2026 16:37 UTC.
Upstream edited
Recorded here
Lines+161added
Lines−154removed
From line
454
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits5to this page, all time
The whole hunk
from line 454, old and new numbered
/
from line 454
454454
455455 ## Building your server
456456
457 ### Importing packages and setting up the instance
457 ### Importing packages and setting up constants
458458
459459 Add these to the top of your `src/index.ts`:
460460
461461 ```typescript theme={null}
462462 import { McpServer } from "@modelcontextprotocol/server";
463 import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
463 import { serveStdio } from "@modelcontextprotocol/server/stdio";
464464 import { z } from "zod";
465465
466466 const NWS_API_BASE = "https://api.weather.gov";
467467 const USER_AGENT = "weather-app/1.0";
468
469 // Create server instance
470 const server = new McpServer({
471 name: "weather",
472 version: "1.0.0",
473 });
474468 ```
475469
476470 ### Helper functions
from line 542
548542
549543 ### Implementing tool execution
550544
551 The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
545 The tool execution handler is responsible for actually executing the logic of each tool. Let's add it, inside a `createServer()` function that builds the server instance and registers both tools on it:
552546
553547 ```typescript theme={null}
554 // Register weather tools
548 // Build a server instance and register its tools. serveStdio calls this
549 // whenever it needs an instance, so every call must return a fresh one.
550 function createServer() {
551 const server = new McpServer({
552 name: "weather",
553 version: "1.0.0",
554 });
555555
556 server.registerTool(
557 "get_alerts",
558 {
559 description: "Get weather alerts for a state",
560 inputSchema: z.object({
561 state: z
562 .string()
563 .length(2)
564 .describe("Two-letter state code (e.g. CA, NY)"),
565 }),
566 },
567 async ({ state }) => {
568 const stateCode = state.toUpperCase();
569 const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
570 const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
556 server.registerTool(
557 "get_alerts",
558 {
559 description: "Get weather alerts for a state",
560 inputSchema: z.object({
561 state: z
562 .string()
563 .length(2)
564 .describe("Two-letter state code (e.g. CA, NY)"),
565 }),
566 },
567 async ({ state }) => {
568 const stateCode = state.toUpperCase();
569 const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
570 const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
571571
572 if (!alertsData) {
573 return {
574 content: [
575 {
576 type: "text",
577 text: "Failed to retrieve alerts data",
578 },
579 ],
580 };
581 }
572 if (!alertsData) {
573 return {
574 content: [
575 {
576 type: "text",
577 text: "Failed to retrieve alerts data",
578 },
579 ],
580 };
581 }
582582
583 const features = alertsData.features || [];
584 if (!features.length) {
583 const features = alertsData.features || [];
584 if (!features.length) {
585 return {
586 content: [
587 {
588 type: "text",
589 text: `No active alerts for ${stateCode}`,
590 },
591 ],
592 };
593 }
594
595 const formattedAlerts = features.map(formatAlert);
596 const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
597
585598 return {
586599 content: [
587600 {
588601 type: "text",
589 text: `No active alerts for ${stateCode}`,
602 text: alertsText,
590603 },
591604 ],
592605 };
593 }
606 },
607 );
594608
595 const formattedAlerts = features.map(formatAlert);
596 const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
609 server.registerTool(
610 "get_forecast",
611 {
612 description: "Get weather forecast for a location",
613 inputSchema: z.object({
614 latitude: z
615 .number()
616 .min(-90)
617 .max(90)
618 .describe("Latitude of the location"),
619 longitude: z
620 .number()
621 .min(-180)
622 .max(180)
623 .describe("Longitude of the location"),
624 }),
625 },
626 async ({ latitude, longitude }) => {
627 // Get grid point data
628 const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
629 const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
597630
598 return {
599 content: [
600 {
601 type: "text",
602 text: alertsText,
603 },
604 ],
605 };
606 },
607 );
631 if (!pointsData) {
632 return {
633 content: [
634 {
635 type: "text",
636 text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
637 },
638 ],
639 };
640 }
608641
609 server.registerTool(
610 "get_forecast",
611 {
612 description: "Get weather forecast for a location",
613 inputSchema: z.object({
614 latitude: z
615 .number()
616 .min(-90)
617 .max(90)
618 .describe("Latitude of the location"),
619 longitude: z
620 .number()
621 .min(-180)
622 .max(180)
623 .describe("Longitude of the location"),
624 }),
625 },
626 async ({ latitude, longitude }) => {
627 // Get grid point data
628 const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
629 const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
642 const forecastUrl = pointsData.properties?.forecast;
643 if (!forecastUrl) {
644 return {
645 content: [
646 {
647 type: "text",
648 text: "Failed to get forecast URL from grid point data",
649 },
650 ],
651 };
652 }
630653
631 if (!pointsData) {
632 return {
633 content: [
634 {
635 type: "text",
636 text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
637 },
638 ],
639 };
640 }
654 // Get forecast data
655 const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
656 if (!forecastData) {
657 return {
658 content: [
659 {
660 type: "text",
661 text: "Failed to retrieve forecast data",
662 },
663 ],
664 };
665 }
641666
642 const forecastUrl = pointsData.properties?.forecast;
643 if (!forecastUrl) {
644 return {
645 content: [
646 {
647 type: "text",
648 text: "Failed to get forecast URL from grid point data",
649 },
650 ],
651 };
652 }
667 const periods = forecastData.properties?.periods || [];
668 if (periods.length === 0) {
669 return {
670 content: [
671 {
672 type: "text",
673 text: "No forecast periods available",
674 },
675 ],
676 };
677 }
653678
654 // Get forecast data
655 const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
656 if (!forecastData) {
657 return {
658 content: [
659 {
660 type: "text",
661 text: "Failed to retrieve forecast data",
662 },
663 ],
664 };
665 }
679 // Format forecast periods
680 const formattedForecast = periods.map((period: ForecastPeriod) =>
681 [
682 `${period.name || "Unknown"}:`,
683 `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
684 `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
685 `${period.shortForecast || "No forecast available"}`,
686 "---",
687 ].join("\n"),
688 );
666689
667 const periods = forecastData.properties?.periods || [];
668 if (periods.length === 0) {
690 const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
691
669692 return {
670693 content: [
671694 {
672695 type: "text",
673 text: "No forecast periods available",
696 text: forecastText,
674697 },
675698 ],
676699 };
677 }
700 },
701 );
678702
679 // Format forecast periods
680 const formattedForecast = periods.map((period: ForecastPeriod) =>
681 [
682 `${period.name || "Unknown"}:`,
683 `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
684 `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
685 `${period.shortForecast || "No forecast available"}`,
686 "---",
687 ].join("\n"),
688 );
689
690 const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
691
692 return {
693 content: [
694 {
695 type: "text",
696 text: forecastText,
697 },
698 ],
699 };
700 },
701 );
703 return server;
704 }
702705 ```
703706
704707 ### Running the server
705708
706 Finally, implement the main function to run the server:
709 Finally, serve the server over stdio:
707710
708711 ```typescript theme={null}
709 async function main() {
710 const transport = new StdioServerTransport();
711 await server.connect(transport);
712 console.error("Weather MCP Server running on stdio");
713 }
714
715 main().catch((error) => {
716 console.error("Fatal error in main():", error);
717 process.exit(1);
712 serveStdio(createServer, {
713 onerror: (error) => console.error("Weather MCP Server error:", error),
718714 });
715 console.error("Weather MCP Server running on stdio");
719716 ```
720717
718 `serveStdio` negotiates the protocol revision with each client, so the same server answers both 2026-07-28 clients and clients that still open with the 2025-era `initialize` handshake. It calls `createServer()` whenever it needs an instance, including once for a `server/discover` probe that is discarded if the client falls back to `initialize`, which is why the factory returns a new `McpServer` on every call. Wiring the server by hand with `server.connect(new StdioServerTransport())` still compiles, but a server started that way only serves 2025-era clients.
719
721720 Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
721
722 To check that the server answers 2026-07-28 clients, list its tools with the [Inspector CLI](/docs/2026-07-28/tools/inspector/cli) in the `modern` [protocol era](/docs/2026-07-28/tools/inspector/protocol-eras):
723
724 ```bash theme={null}
725 npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era modern --method tools/list
726 ```
727
728 The Inspector defaults to the `legacy` era, which only exercises the 2025-era handshake, so run the same command once more with `--protocol-era legacy`. Both runs list `get_alerts` and `get_forecast`.
722729
723730 Let's now test your server from an existing MCP host, Claude for Desktop.
724731
No line in this hunk matches that.