Skip to content

Linking with External Tools

Connecting Your MCP Server to the Outside World

Where We Left Off

In the previous chapter you built a self-running MCP server with two tools, add and greet, and connected VS Code Copilot to it as the MCP client. Both tools were self-contained: they computed everything locally.

In this chapter we go further: your server's tools will call external APIs and other MCP servers, turning your server into a hub that bridges Copilot to the outside world.

Before:   VS Code Copilot → Your MCP Server → local logic
Now:      VS Code Copilot → Your MCP Server → external APIs

Calling an External API from a Tool

The simplest external link is calling a public HTTP API directly from inside a tool handler.

We will use Open-Meteo - a free, no-API-key-required weather API.

🔗 open-meteo.com - free, no sign-up, no API key, no rate limit for basic use.

  • Node.js — fetch is built into Node 18+, no new package needed.
  • .NET — HttpClient ships with the framework, no new package needed. Register it once on the DI container with builder.Services.AddHttpClient() and you can inject it into any tool method.
  • Python — httpx is one small dependency. Add httpx>=0.27 to requirements.txt and run pip install -r requirements.txt. It gives us an async HTTP client that fits FastMCP's async tool handlers.
text
mcp>=1.27.0
httpx>=0.27

Section 1 - The tool definition and schema

We'll register one tool in this part:

  • get_weather: takes a city name and returns the current temperature and wind speed. It resolves the city to coordinates using Open-Meteo's free geocoding API, then fetches the current weather for those coordinates. Two API calls, no key required.
javascript
server.registerTool(
  "get_weather",
  {
    description: "Returns the current temperature for a given city by name",
    inputSchema: {
      city: z.string().describe("Name of the city"),
    },
  },
  // handler — see Section 2
);
csharp
// 1. Register HttpClient on the DI container, alongside the existing services
builder.Services.AddHttpClient();

builder.Services
    .AddMcpServer(o =>
    {
        o.ServerInfo = new() { Name = "devcon-workshop-server", Version = "1.0.0" };
    })
    .WithHttpTransport(options => options.Stateless = true)
    .WithTools<MathTools>()
    .WithTools<GreetingTools>()
    .WithTools<WeatherTools>();

// 2. Add the new tools class alongside the existing ones
[McpServerToolType]
public class WeatherTools
{
    [McpServerTool(Name = "get_weather"),
     Description("Returns the current temperature for a given city by name")]
    public static async Task<string> GetWeather(
        HttpClient http,
        [Description("Name of the city")] string city)
    {
        // handler — see Section 2
    }
}
python
# Add httpx to the imports at the top of the file
import httpx
from typing import Annotated, Any, Literal

@mcp.tool(description="Returns the current temperature for a given city by name")
async def get_weather(
    city: Annotated[str, Field(description="Name of the city")],
) -> str:
    ...  # handler — see Section 2

The tool only needs a city name. The handler resolves coordinates internally using Open-Meteo's free geocoding API, so callers — including LLM agents — just say "Amsterdam" and the tool handles the rest.

  • Node.js: inputSchema declares city as a string with z.string(). The handler receives { city } already validated.
  • .NET: the parameter string city is the schema. The HttpClient http parameter is supplied by the SDK from the DI container — any non-[Description] parameter is resolved as a service.
  • Python: the city: Annotated[str, Field(...)] parameter is the schema. The handler is declared async def so it can await the HTTP calls — FastMCP supports both sync and async tools.

Section 2 - The handler: calling the external API

javascript
async ({ city }) => {
  // Step 1: resolve city name to coordinates using Open-Meteo's free geocoding API
  const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`;
  const geoRes = await fetch(geoUrl);
  const geoData = await geoRes.json();
  if (!geoData.results?.length) {
    return { content: [{ type: "text", text: `City not found: ${city}` }] };
  }
  const { latitude, longitude } = geoData.results[0];

  // Step 2: fetch current weather for those coordinates
  const url =
    `https://api.open-meteo.com/v1/forecast` +
    `?latitude=${latitude}&longitude=${longitude}&current_weather=true`;

  const response = await fetch(url);
  const data = await response.json();

  const temp = data.current_weather.temperature;
  const wind = data.current_weather.windspeed;

  return {
    content: [
      {
        type: "text",
        text: `Weather in ${city}: ${temp}°C, wind ${wind} km/h`,
      },
    ],
  };
},
csharp
public static async Task<string> GetWeather(
    HttpClient http,
    [Description("Name of the city")] string city)
{
    // Step 1: resolve city name to coordinates using Open-Meteo's free geocoding API
    var geoData = await http.GetFromJsonAsync<JsonElement>(
        $"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(city)}&count=1");

    if (!geoData.TryGetProperty("results", out var results) || results.GetArrayLength() == 0)
        return $"City not found: {city}";

    var lat = results[0].GetProperty("latitude").GetDouble();
    var lon = results[0].GetProperty("longitude").GetDouble();

    // Step 2: fetch current weather for those coordinates
    var data = await http.GetFromJsonAsync<JsonElement>(
        $"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current_weather=true");

    var current = data.GetProperty("current_weather");
    var temp = current.GetProperty("temperature").GetDouble();
    var wind = current.GetProperty("windspeed").GetDouble();

    return $"Weather in {city}: {temp}°C, wind {wind} km/h";
}
python
@mcp.tool(description="Returns the current temperature for a given city by name")
async def get_weather(
    city: Annotated[str, Field(description="Name of the city")],
) -> str:
    async with httpx.AsyncClient(timeout=10.0) as client:
        # Step 1: resolve city name to coordinates using Open-Meteo's free geocoding API
        geo = await client.get(
            "https://geocoding-api.open-meteo.com/v1/search",
            params={"name": city, "count": 1},
        )
        geo.raise_for_status()
        results: list[dict[str, Any]] = geo.json().get("results") or []
        if not results:
            return f"City not found: {city}"

        latitude = results[0]["latitude"]
        longitude = results[0]["longitude"]

        # Step 2: fetch current weather for those coordinates
        forecast = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": latitude,
                "longitude": longitude,
                "current_weather": "true",
            },
        )
        forecast.raise_for_status()
        current = forecast.json()["current_weather"]

    return f"Weather in {city}: {current['temperature']}°C, wind {current['windspeed']} km/h"

Node.js

  • fetch is the built-in global from Node 18+, no import needed.
  • geoData.results?.length handles the "city not found" case with a friendly response instead of an exception.
  • The handler returns a content array — the same shape every MCP tool returns.

.NET

  • System.Net.Http.Json (add using System.Net.Http.Json; at the top of the file) provides GetFromJsonAsync<JsonElement>, which fetches and parses JSON in one call.
  • JsonElement gives dynamic access without defining DTOs for either Open-Meteo response.
  • Returning a plain string is shorthand — the SDK wraps it into the standard content: [{ type: "text", text: ... }] payload for you.

Python

  • async with httpx.AsyncClient(...) opens one client for both calls and closes it cleanly when the block exits. The timeout=10.0 guards against a hung request.
  • geo.json().get("results") or [] handles the "city not found" case with a friendly response instead of an exception.
  • raise_for_status() turns any HTTP error into an exception the SDK reports back to the client.
  • Returning a plain str is shorthand — the SDK wraps it into the standard content: [{ type: "text", text: ... }] payload for you.

Two API calls, no key required — both Open-Meteo endpoints are free. The client sees a normal tool response and has no idea what happened inside.

View complete server.js in Source Code → · View complete Server.cs in Source Code → · View complete server.py in Source Code →

Try it in Copilot Chat

Step 1 - Start the server

bash
node server.js
bash
dotnet run
bash
python server.py

Step 2 - Restart the MCP connection

Open the Command Palette and run MCP: List Servers → Restart.

Step 3 - Ask Copilot

Open Copilot Chat in Agent mode and ask:

What's the weather in Amsterdam?

Copilot calls your get_weather tool and returns the result.

DevCon MCP Workshop