mcp-app-openweather

mcp-app-openweather

An MCP server that provides weather forecasts from OpenWeather and renders interactive charts in chat via MCP Apps, letting users query forecasts and switch units, day range, and metrics.

Category
访问服务器

README

MCP App: OpenWeather Forecast Visualizer

A proof-of-concept MCP App — an MCP server whose tool renders an interactive chart inside the chat instead of returning only text. Ask for a forecast, get a Recharts graph you can click; clicking a control calls back into the server for fresh data.

Built as a readable reference for the MCP Apps extension, written against @modelcontextprotocol/ext-apps@1.7.5 and @modelcontextprotocol/sdk@1.30.0. The extension is under active development; _meta.ui shapes may shift, so treat the pinned versions as part of the API.

What an MCP App is, in one screen

A normal MCP tool returns content the model reads. An MCP App additionally ships a UI resource that the host renders in a sandboxed iframe. Two registrations tied together by one URI:

// server.ts
const RESOURCE_URI = "ui://weather-forecast/mcp-app.html";

registerAppTool(server, TOOL_NAME, {
  inputSchema: { location: z.string(), units: ..., days: ... },
  _meta: { ui: { resourceUri: RESOURCE_URI } },   // <-- the link
}, handler);

registerAppResource(server, "Weather Forecast Chart", RESOURCE_URI, {
  mimeType: RESOURCE_MIME_TYPE,
}, () => /* the built HTML */);

Drop _meta.ui.resourceUri and everything still "works" — the tool silently degrades to plain text with no UI. That failure is invisible, which is why tests/server/server.test.ts asserts on it explicitly.

Three things worth internalizing from this codebase:

  1. The payload split. The handler returns content[0].text (a short summary — cheap tokens for the model) and structuredContent (the full typed series — for the chart). Never make the model pay for 40 data points it won't read. See server.ts.
  2. The UI is a client, not a template. src/mcp-app.tsx uses useApp() and calls app.callServerTool(...) when you change units or day range. That round trip is the entire reason to reach for MCP Apps.
  3. ui:// resources declare their own CSP. Everything is inlined into one HTML file except the weather icons, so the resource declares _meta.ui.csp.resourceDomains: ["https://openweathermap.org"] — a small deliberate example of the field.

Setup

npm install
cp .env.example .env      # then paste your key in
npm run build             # typechecks, then bundles the UI into dist/mcp-app.html

The free OpenWeather tier is enough. New keys take ~10 minutes to activate; until then every call returns 401.

Running it

Two transports, one entry point, to show both ends of the spectrum:

npm run serve:stdio   # stdio -- simplest local wiring, host spawns the process
npm run serve         # Streamable HTTP on :3001/mcp -- shareable, tunnelable
npm run dev           # rebuild UI on change + serve over HTTP

stdio gotcha: stdout is the JSON-RPC channel. One stray console.log corrupts the stream and the host reports an unparseable message. Every diagnostic in main.ts uses console.error.

The HTTP mode is stateless — sessionIdGenerator: undefined, with a fresh server and transport per request, closed on res.close. Nothing to reap.

Sharing the HTTP server as a custom connector:

npx cloudflared tunnel --url http://localhost:3001
# add the resulting https URL + /mcp as a custom connector

Adding it to the Claude desktop app

Written for someone who has never set up an MCP server. A Claude Pro account is enough. Claude Desktop supports the MCP Apps extension, so the chart renders inline rather than falling back to text.

You'll wire this up as a local stdio server: Claude Desktop starts the process itself, so there's no hosting, no tunnel, and no login. Roughly 10 minutes.

1. Install the prerequisites

  • Claude Desktop — https://claude.ai/download. If it's already installed, use Check for Updates… in the Claude menu.

  • Node.js 20+ — https://nodejs.org (the LTS build). Verify in a terminal:

    node --version
    

2. Set the project up

git clone https://github.com/kasza-introhive/mcp-app-openweather.git
cd mcp-app-openweather
npm install
npm run build      # REQUIRED -- creates dist/mcp-app.html, which IS the chart

Skipping npm run build is the most common mistake: the server starts fine and the tool returns data, but asking for the UI errors out because the bundle it serves doesn't exist yet.

3. Add your OpenWeather API key

Sign up at https://openweathermap.org/api and copy your key from https://home.openweathermap.org/api_keys — the free tier is all this needs.

cp .env.example .env

Open .env and paste the key after OPENWEATHER_API_KEY=. A brand-new key can take ~10 minutes to activate; until then every request returns 401.

4. Find your absolute paths

Claude Desktop doesn't run with your shell's PATH, so both the Node binary and the project directory must be spelled out in full. Print them:

command -v node   # e.g. /usr/local/bin/node   (Windows: where node)
pwd               # e.g. /Users/you/code/mcp-app-openweather

5. Edit the Claude Desktop config

Open the Claude menu in your OS menu bar (not the settings inside the chat window) → Settings… → Developer tab → Edit Config. That creates or opens:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add this, substituting the two paths from step 4:

{
  "mcpServers": {
    "openweather-forecast": {
      "command": "/absolute/path/to/node",
      "args": [
        "--env-file-if-exists=.env",
        "--import", "tsx",
        "main.ts",
        "--stdio"
      ],
      "cwd": "/absolute/path/to/mcp-app-openweather"
    }
  }
}

If the file already has an mcpServers block, add "openweather-forecast" alongside your existing entries rather than replacing them.

Two things here are load-bearing, and both cause failures that look like something else:

  • Call node directly, not npm run serve:stdio. npm prints a two-line banner to stdout, and under stdio stdout is the JSON-RPC channel — the banner corrupts the first message and the server appears to fail for no reason. The npm scripts are for humans in a terminal.
  • cwd is required. --import tsx resolves tsx from the working directory, so without it Node exits with ERR_MODULE_NOT_FOUND.

On Windows, write paths with escaped backslashes: "C:\\Program Files\\nodejs\\node.exe".

6. Restart and confirm

Quit Claude Desktop completely (⌘Q / right-click → Quit — closing the window isn't enough) and reopen it. Config is only read at startup.

Click the Add files, connectors, and more (/) control at the bottom-left of the message box, hover Connectors → Manage connectors, and look for openweather-forecast with get-forecast listed under it.

7. Ask for a forecast

"What's the weather forecast for Halifax this week?"

Claude will ask permission to run get-forecast the first time — approve it. An interactive chart appears in the conversation. Click Humidity to reshape the data instantly (no server call), or °F / 1 day to trigger a fresh tools/call.

If it doesn't show up

Read the logs first — they name the actual cause:

# macOS
tail -n 50 -f ~/Library/Logs/Claude/mcp-server-openweather-forecast.log

# Windows
type "%APPDATA%\Claude\logs\mcp-server-openweather-forecast.log"
Symptom Cause
Connector absent entirely Invalid JSON in the config (a trailing comma is the usual culprit), or Claude wasn't fully quit
ERR_MODULE_NOT_FOUND cwd missing or wrong
spawn ENOENT The command path is wrong — re-run command -v node
Unparseable-message errors Launched via npm instead of node (see step 5)
Tool errors mentioning OPENWEATHER_API_KEY .env missing, key not pasted, or key not yet activated
Chart never renders but text works npm run build wasn't run, so dist/mcp-app.html is absent

To confirm the server itself is healthy, independent of Claude, run it by hand from the project directory — it should print one line of JSON:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"1"}}}' \
  | node --env-file-if-exists=.env --import tsx main.ts --stdio

Try it

"What's the weather forecast for Halifax this week?"

The chart appears in the chat. Switching metric (temp / humidity / wind / pressure) reshapes the data already on the client — no server call. Switching units or day range issues a real tools/call, because only the server has that data.

Layout

main.ts                     transport selection: --stdio | Streamable HTTP
server.ts                   createServer(): the tool + the ui:// resource
src/openweather.ts          the ONLY module that talks to OpenWeather
src/mcp-app.tsx             React root, useApp(), callServerTool
src/components/             ForecastChart (Recharts), Controls
tests/unit/                 openweather: transform, schemas, error mapping
tests/server/               the MCP boundary, over an in-memory transport
tests/e2e/                  built bundle + a fake host bridge
tests/contract/             opt-in live API call (schema drift)

src/openweather.ts validates both upstream responses with zod before use — it's a trust boundary — and maps failures to messages that never interpolate the response body, since OpenWeather can echo the appid back. There's a test asserting the key never appears in an error message.

Testing

npm test          # unit + server. Hermetic: MSW intercepts all HTTP.
npm run test:e2e  # Playwright against dist/mcp-app.html (run npm run build first)
npm run test:live # opt-in: one REAL API call, asserts schema only

MSW runs with onUnhandledRequest: "error", so an accidental real network call fails the suite rather than passing quietly.

tests/e2e/fake-host.ts is a ~100-line host implementation and doubles as protocol documentation: it answers ui/initialize, pushes ui/notifications/tool-result, and serves tools/call from a queue while recording each call. Three things that cost real debugging time:

  • McpUiInitializeResult requires protocolVersion, hostInfo, hostCapabilities (not capabilities), and hostContext. Get one wrong and the app renders "Could not connect to the host".
  • The tool-result notification's params is the tool result — content/structuredContent sit at the top level of params, not nested under a result key.
  • The app must run in an iframe. It posts to window.parent, so loaded top-level it just talks to itself. Hence tests/e2e/harness.html. And because addInitScript runs in every frame, the fake host bails out with if (window.parent !== window) return; or the two frames ping-pong forever.

On a restricted machine where Chromium can't do its normal multi-process launch, set PW_SINGLE_PROCESS=1 (see playwright.config.ts). Note that under --single-process the browser doesn't survive closing a context, so only the first test per worker runs — pass -g "<test title>" to run them one at a time.

Verifying against a real host

The fake bridge proves this app's logic, not host compatibility — it doesn't enforce the iframe sandbox or CSP. For that:

git clone https://github.com/modelcontextprotocol/ext-apps
cd ext-apps/examples/basic-host && npm install
SERVERS='["http://localhost:3001/mcp"]' npm start   # then open localhost:8080

Confirm the chart draws, the icons load (that's the CSP allowance working), and a control change shows a new tools/call in the server log.

Why forecast and not history

The original goal was historical weather. OpenWeather's historical endpoints (One Call 3.0 timemachine and day_summary) need a separate paid "One Call by Call" subscription and return 401 on a free-tier key. So this uses the free 5 day / 3 hour forecast instead. Don't spend an afternoon rediscovering that.

POC caveats

  • Bundle size. vite-plugin-singlefile inlines React and Recharts into one ~920 KB HTML resource. Fine for a demo, not a production recommendation — a real app would trim the chart library or load from a CDN with the right resourceDomains.
  • updateModelContext is deliberately unimplemented. src/mcp-app.tsx carries a commented reportExploration() stub marking where the app would tell the model what the user explored. Left as an exercise.
  • No auth, no caching, no in-app location search.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选