mcpのクイックスタートの内容をGoに書き換えてMCPサーバーを起動してみる
クイックスタートはpythonで書かれていたので、claudeにgoに書き直してもらった。
goのライブラリなどありそうだけど、標準パッケージのみで作ってくれたので、それはそれで動作を詳細まで把握できるので一旦はありがたい。
また別途ライブラリは探してみる。
package main import ( "bufio" "context" "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" ) // MCP Protocol structures type JSONRPCRequest struct { JSONRPC string `json:"jsonrpc"` ID interface{} `json:"id"` Method string `json:"method"` Params interface{} `json:"params,omitempty"` } type JSONRPCResponse struct { JSONRPC string `json:"jsonrpc"` ID interface{} `json:"id"` Result interface{} `json:"result,omitempty"` Error *RPCError `json:"error,omitempty"` } type RPCError struct { Code int `json:"code"` Message string `json:"message"` } type ServerInfo struct { Name string `json:"name"` Version string `json:"version"` Capabilities map[string]interface{} `json:"capabilities"` } type Tool struct { Name string `json:"name"` Description string `json:"description"` InputSchema interface{} `json:"inputSchema"` } type ToolsResponse struct { Tools []Tool `json:"tools"` } // Weather API structures type NWSAlert struct { Properties struct { Event string `json:"event"` AreaDesc string `json:"areaDesc"` Severity string `json:"severity"` Description string `json:"description"` Instruction string `json:"instruction"` } `json:"properties"` } type NWSAlertsResponse struct { Features []NWSAlert `json:"features"` } type NWSPointsResponse struct { Properties struct { Forecast string `json:"forecast"` } `json:"properties"` } type NWSForecastPeriod struct { Name string `json:"name"` Temperature int `json:"temperature"` TemperatureUnit string `json:"temperatureUnit"` WindSpeed string `json:"windSpeed"` WindDirection string `json:"windDirection"` DetailedForecast string `json:"detailedForecast"` } type NWSForecastResponse struct { Properties struct { Periods []NWSForecastPeriod `json:"periods"` } `json:"properties"` } // WeatherMCPServer represents the MCP server type WeatherMCPServer struct { name string version string httpClient *http.Client } // Constants const ( NWSAPIBase = "https://api.weather.gov" UserAgent = "weather-app/1.0" ) func NewWeatherMCPServer() *WeatherMCPServer { return &WeatherMCPServer{ name: "weather", version: "1.0.0", httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } func (s *WeatherMCPServer) makeNWSRequest(ctx context.Context, url string, target interface{}) error { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } req.Header.Set("User-Agent", UserAgent) req.Header.Set("Accept", "application/geo+json") resp, err := s.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("API request failed with status: %d", resp.StatusCode) } return json.NewDecoder(resp.Body).Decode(target) } func (s *WeatherMCPServer) formatAlert(alert NWSAlert) string { props := alert.Properties event := props.Event if event == "" { event = "Unknown" } areaDesc := props.AreaDesc if areaDesc == "" { areaDesc = "Unknown" } severity := props.Severity if severity == "" { severity = "Unknown" } description := props.Description if description == "" { description = "No description available" } instruction := props.Instruction if instruction == "" { instruction = "No specific instructions provided" } return fmt.Sprintf(` Event: %s Area: %s Severity: %s Description: %s Instructions: %s `, event, areaDesc, severity, description, instruction) } func (s *WeatherMCPServer) getAlerts(state string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() url := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, strings.ToUpper(state)) var alertsResponse NWSAlertsResponse err := s.makeNWSRequest(ctx, url, &alertsResponse) if err != nil { return "Unable to fetch alerts or no alerts found.", nil } if len(alertsResponse.Features) == 0 { return "No active alerts for this state.", nil } var alerts []string for _, feature := range alertsResponse.Features { alerts = append(alerts, s.formatAlert(feature)) } return strings.Join(alerts, "\n---\n"), nil } func (s *WeatherMCPServer) getForecast(latitude, longitude float64) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // First get the forecast grid endpoint pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, latitude, longitude) var pointsResponse NWSPointsResponse err := s.makeNWSRequest(ctx, pointsURL, &pointsResponse) if err != nil { return "Unable to fetch forecast data for this location.", nil } // Get the forecast URL from the points response forecastURL := pointsResponse.Properties.Forecast if forecastURL == "" { return "Unable to get forecast URL for this location.", nil } var forecastResponse NWSForecastResponse err = s.makeNWSRequest(ctx, forecastURL, &forecastResponse) if err != nil { return "Unable to fetch detailed forecast.", nil } // Format the periods into a readable forecast periods := forecastResponse.Properties.Periods var forecasts []string // Only show next 5 periods maxPeriods := len(periods) if maxPeriods > 5 { maxPeriods = 5 } for i := 0; i < maxPeriods; i++ { period := periods[i] forecast := fmt.Sprintf(` %s: Temperature: %d°%s Wind: %s %s Forecast: %s `, period.Name, period.Temperature, period.TemperatureUnit, period.WindSpeed, period.WindDirection, period.DetailedForecast) forecasts = append(forecasts, forecast) } return strings.Join(forecasts, "\n---\n"), nil } func (s *WeatherMCPServer) handleRequest(req JSONRPCRequest) JSONRPCResponse { switch req.Method { case "initialize": return s.handleInitialize(req) case "tools/list": return s.handleListTools(req) case "tools/call": return s.handleCallTool(req) default: return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Error: &RPCError{ Code: -32601, Message: "Method not found", }, } } } func (s *WeatherMCPServer) handleInitialize(req JSONRPCRequest) JSONRPCResponse { serverInfo := ServerInfo{ Name: s.name, Version: s.version, Capabilities: map[string]interface{}{ "tools": map[string]interface{}{}, }, } return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: serverInfo, } } func (s *WeatherMCPServer) handleListTools(req JSONRPCRequest) JSONRPCResponse { tools := []Tool{ { Name: "get_alerts", Description: "Get weather alerts for a US state.", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "state": map[string]interface{}{ "type": "string", "description": "Two-letter US state code (e.g. CA, NY)", }, }, "required": []string{"state"}, }, }, { Name: "get_forecast", Description: "Get weather forecast for a location.", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "latitude": map[string]interface{}{ "type": "number", "description": "Latitude of the location", }, "longitude": map[string]interface{}{ "type": "number", "description": "Longitude of the location", }, }, "required": []string{"latitude", "longitude"}, }, }, } response := ToolsResponse{Tools: tools} return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: response, } } func (s *WeatherMCPServer) handleCallTool(req JSONRPCRequest) JSONRPCResponse { params := req.Params.(map[string]interface{}) name := params["name"].(string) arguments := params["arguments"].(map[string]interface{}) switch name { case "get_alerts": return s.callGetAlerts(req.ID, arguments) case "get_forecast": return s.callGetForecast(req.ID, arguments) default: return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Error: &RPCError{ Code: -32602, Message: "Unknown tool", }, } } } func (s *WeatherMCPServer) callGetAlerts(id interface{}, args map[string]interface{}) JSONRPCResponse { state, ok := args["state"].(string) if !ok { return JSONRPCResponse{ JSONRPC: "2.0", ID: id, Error: &RPCError{ Code: -32602, Message: "Invalid state parameter", }, } } result, err := s.getAlerts(state) if err != nil { return JSONRPCResponse{ JSONRPC: "2.0", ID: id, Result: map[string]interface{}{ "content": []map[string]interface{}{ { "type": "text", "text": fmt.Sprintf("Error: %s", err.Error()), }, }, "isError": true, }, } } return JSONRPCResponse{ JSONRPC: "2.0", ID: id, Result: map[string]interface{}{ "content": []map[string]interface{}{ { "type": "text", "text": result, }, }, }, } } func (s *WeatherMCPServer) callGetForecast(id interface{}, args map[string]interface{}) JSONRPCResponse { latitude, latOk := args["latitude"].(float64) longitude, lonOk := args["longitude"].(float64) if !latOk || !lonOk { return JSONRPCResponse{ JSONRPC: "2.0", ID: id, Error: &RPCError{ Code: -32602, Message: "Invalid latitude or longitude parameter", }, } } result, err := s.getForecast(latitude, longitude) if err != nil { return JSONRPCResponse{ JSONRPC: "2.0", ID: id, Result: map[string]interface{}{ "content": []map[string]interface{}{ { "type": "text", "text": fmt.Sprintf("Error: %s", err.Error()), }, }, "isError": true, }, } } return JSONRPCResponse{ JSONRPC: "2.0", ID: id, Result: map[string]interface{}{ "content": []map[string]interface{}{ { "type": "text", "text": result, }, }, }, } } func (s *WeatherMCPServer) Run() { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { line := scanner.Text() if line == "" { continue } var req JSONRPCRequest if err := json.Unmarshal([]byte(line), &req); err != nil { log.Printf("JSON parse error: %v", err) continue } response := s.handleRequest(req) responseBytes, err := json.Marshal(response) if err != nil { log.Printf("JSON marshal error: %v", err) continue } fmt.Println(string(responseBytes)) } if err := scanner.Err(); err != nil { log.Printf("Scanner error: %v", err) } } func main() { server := NewWeatherMCPServer() server.Run() }
コードの中身は後でちゃんと見るとして、これで動くのかを確認する。
mcpサーバーに追加。
クイックスタートではclaude for Desktopで動作するように、macのclaudeのアプリケーションの設定ファイルをいじってみたが、今回はclaudeのcliから実行できるように、claudeコマンドからmcpサーバーの追加を試みる。
$ claude mcp add weather-server "go run main.go" Added stdio MCP server weather-server with command: go run main.go to local config
ログによると、local configに設定されている模様。
しかし、実行ディレクトリにそれっぽいファイルが生成されていない。
どこから読み込んでいるだろう。。
しかし、設定が追加されていることは確認できた。
$ claude mcp list weather-server: go run main.go
一旦設定ファイルがどこにあるの問題はさておき、動かしてみる。
指定方法がフルパスじゃなくてこれでいいのかは気になるところ。
実行できるか確認してみる
動作確認
初期化リクエスト
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | go run main.go
{"jsonrpc":"2.0","id":1,"result":{"name":"weather","version":"1.0.0","capabilities":{"tools":{}}}}
ツール一覧取得
go-weather-scratch <go-weather-scratch>% echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | go run main.go
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_alerts","description":"Get weather alerts for a US state.","inputSchema":{"properties":{"state":{"description":"Two-letter US state code (e.g. CA, NY)","type":"string"}},"required":["state"],"type":"object"}},{"name":"get_forecast","description":"Get weather forecast for a location.","inputSchema":{"properties":{"latitude":{"description":"Latitude of the location","type":"number"},"longitude":{"description":"Longitude of the location","type":"number"}},"required":["latitude","longitude"],"type":"object"}}]}}
天気アラート取得テスト
go-weather-scratch <go-weather-scratch>% echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_alerts","arguments":{"state":"CA"}}}' | go run main.go
{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"\nEvent: Red Flag Warning\nArea: Modoc County Except for the Surprise Valley; Klamath Basin and the Fremont-Winema National Forest; South Central Oregon Desert including the BLM Land in Eastern Lake and Western Harney Counties\nSeverity: Severe\nDescription: The National Weather Service in Medford has issued a Red Flag\nWarning, which is in effect from 2 PM to 8 PM PDT Wednesday.\n\n* IMPACTS...Any fires that develop will likely spread rapidly.\n\n* AFFECTED AREA...In CAZ285...Fire weather zone 285.In ORZ624...\nFire weather zone 624.In ORZ625...Fire weather zone 625.\n\n* THUNDERSTORMS...Scattered afternoon and evening thunderstorms,\nmainly along and eats of the highway 97 corridor.\n\n* OUTFLOW WINDS...Gusts up to 45 mph. These outflow winds can\ntravel up to 25 miles away from the thunderstorm that caused it.\n\n* DETAILEDURL...View the hazard area in detail at\nhttps://www.wrh.noaa.gov/map/?wfo=mfr\nInstructions: Follow all fire restrictions. You can find your county's\nemergency sign up form as well as links to fire restrictions at\nweather.gov/medford/wildfire. One less spark, one less wildfire.\n\nBe sure you're signed up for your county's emergency alert\nsystem. Familiarize yourself with your emergency plan and make\nsure you listen to emergency services. Visit ready.gov/plan for\nmore information.\n\nA Red Flag Warning is issued when we identify weather conditions\nthat promote rapid spread of fire which may become life-\nthreatening. This does not mean there is a fire. These conditions\nare either occurring now or will begin soon. It is important to\nhave multiple ways to receive information from authorities.\n\n---\n\nEvent: Red Flag Warning\nArea: Siskiyou County from the Cascade Mountains East and South to Mt Shasta\nSeverity: Severe\nDescription: The National Weather Service in Medford has issued a Red Flag\nWarning, which is in effect from 2 PM to 8 PM PDT Wednesday.\n\n* IMPACTS...Given the long stretch of dry and hot and very\nconditions, lightning efficiency will be moderate for new fire\nstarts. Any fires that develop will likely spread rapidly.\n\n* AFFECTED AREA...All of Fire Weather Zone 284.\n\n* THUNDERSTORMS...Scattered afternoon and evening thunderstorms\nacross the region.\n\n* OUTFLOW WINDS...Gusts up to 45 mph. These outflow winds can\ntravel up to 25 miles away from the thunderstorm that caused it.\n\n* ADDITIONAL INFORMATION...Some storms could be dry with gusty\noutflows.\n\n* DETAILED URL...View the hazard area in detail at\nhttps://www.wrh.noaa.gov/map/?wfo=mfr\nInstructions: Follow all fire restrictions. You can find your county's\nemergency sign up form as well as links to fire restrictions at\nweather.gov/medford/wildfire. One less spark, one less wildfire.\n\nBe sure you're signed up for your county's emergency alert\nsystem. Familiarize yourself with your emergency plan and make\nsure you listen to emergency services. Visit ready.gov/plan for\nmore information.\n\nA Red Flag Warning is issued when we identify weather conditions\nthat promote rapid spread of fire which may become life-\nthreatening. This does not mean there is a fire. These conditions\nare either occurring now or will begin soon. It is important to\nhave multiple ways to receive information from authorities.\n","type":"text"}]}}
天気予報取得テスト
go-weather-scratch <go-weather-scratch>% echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_forecast","arguments":{"latitude":37.7749,"longitude":-122.4194}}}' | go run main.go
{"jsonrpc":"2.0","id":4,"result":{"content":[{"text":"\nOvernight:\nTemperature: 54°F\nWind: 13 mph WSW\nForecast: Mostly cloudy, with a low around 54. West southwest wind around 13 mph.\n\n---\n\nWednesday:\nTemperature: 66°F\nWind: 10 to 21 mph WSW\nForecast: Mostly sunny. High near 66, with temperatures falling to around 64 in the afternoon. West southwest wind 10 to 21 mph, with gusts as high as 25 mph.\n\n---\n\nWednesday Night:\nTemperature: 54°F\nWind: 15 to 21 mph WSW\nForecast: Mostly clear, with a low around 54. West southwest wind 15 to 21 mph, with gusts as high as 26 mph.\n\n---\n\nThursday:\nTemperature: 68°F\nWind: 13 to 23 mph W\nForecast: Mostly sunny, with a high near 68. West wind 13 to 23 mph, with gusts as high as 28 mph.\n\n---\n\nThursday Night:\nTemperature: 54°F\nWind: 16 to 22 mph W\nForecast: Partly cloudy, with a low around 54. West wind 16 to 22 mph, with gusts as high as 28 mph.\n","type":"text"}]}}
それぞれ正常に動いていそうではある。
claudeで動かしてみる
claude
以下のように、mcpサーバーと接続ができていない模様
1 MCP server failed to connect (see /mcp for info)
/mcp
Manage MCP servers │ │ │ │ ❯ 1. weather-server ✘ failed · Enter to view details │ │ │ │ ※ Tip: Run claude --debug to see logs inline, or view log files in │ │ /Users/yoshii/Library/Caches/claude-cli-nodejs/-Users-yoshii-src-github-com-my0shym-mcp-practice-202507-go-weather-scratch
claude --debug
ログの一部
[DEBUG] MCP server "weather-server": Connection failed: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"protocolVersion"
],
"message": "Required"
},
{
"code": "invalid_type",
"expected": "object",
"received": "undefined",
"path": [
"serverInfo"
],
"message": "Required"
}
]
[DEBUG] MCP server "weather-server": Error message: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"protocolVersion"
],
"message": "Required"
},
{
"code": "invalid_type",
"expected": "object",
"received": "undefined",
"path": [
"serverInfo"
],
"message": "Required"
}
]
[DEBUG] MCP server "weather-server": Error stack: ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"protocolVersion"
],
"message": "Required"
},
{
"code": "invalid_type",
"expected": "object",
"received": "undefined",
"path": [
"serverInfo"
],
"message": "Required"
}
]
at get error [as error] (file:///Users/yoshii/.anyenv/envs/nodenv/versions/20.6.1/lib/node_modules/@anthropic-ai/claude-code/cli.js:583:10459)
at i3.parse (file:///Users/yoshii/.anyenv/envs/nodenv/versions/20.6.1/lib/node_modules/@anthropic-ai/claude-code/cli.js:583:11835)
at file:///Users/yoshii/.anyenv/envs/nodenv/versions/20.6.1/lib/node_modules/@anthropic-ai/claude-code/cli.js:1336:18765
at Ln1._onresponse (file:///Users/yoshii/.anyenv/envs/nodenv/versions/20.6.1/lib/node_modules/@anthropic-ai/claude-code/cli.js:1336:17359)
at _transport.onmessage (file:///Users/yoshii/.anyenv/envs/nodenv/versions/20.6.1/lib/node_modules/@anthropic-ai/claude-code/cli.js:1336:14528)
at kn1.processReadBuffer (file:///Users/yoshii/.anyenv/envs/nodenv/versions/20.6.1/lib/node_modules/@anthropic-ai/claude-code/cli.js:1338:2205)
at Socket.<anonymous> (file:///Users/yoshii/.anyenv/envs/nodenv/versions/20.6.1/lib/node_modules/@anthropic-ai/claude-code/cli.js:1338:1675)
at Socket.emit (node:events:514:28)
at Socket.emit (node:domain:489:12)
at addChunk (node:internal/streams/readable:343:12)
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ > Try "refactor <filepath>" │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
? for shortcuts ◯ Use /ide to connect to your IDE
Debug mode
[ERROR] MCP server "weather-server" Connection failed: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"protocolVersion"
],
"message": "Required"
},
{
"code": "invalid_type",
[DEBUG] Saving global config to /Users/yoshii/.claude.json
[DEBUG] Re-reading config from /Users/yoshii/.claude.json
[DEBUG] Reading config from /Users/yoshii/.claude.json
[DEBUG] Config parsed successfully from /Users/yoshii/.claude.json
[DEBUG] Writing config to /Users/yoshii/.claude.json
[DEBUG] Config written successfully
[DEBUG] Reading config from /Users/yoshii/.claude.json
[DEBUG] Config parsed successfully from /Users/yoshii/.claude.json
[DEBUG] Saving global config to /Users/yoshii/.claude.json
[DEBUG] Re-reading config from /Users/yoshii/.claude.json
[DEBUG] Reading config from /Users/yoshii/.claude.json
[DEBUG] Config parsed successfully from /Users/yoshii/.claude.json
[DEBUG] Writing config to /Users/yoshii/.claude.json
[DEBUG] Config written successfully
[DEBUG] Reading config from /Users/yoshii/.claude.json
[DEBUG] Config parsed successfully from /Users/yoshii/.claude.json
[DEBUG] AutoUpdaterWrapper: Installation type: npm-global, using native: false
これをclaudeに食わせたところ、
MCPのinitializeレスポンスの形式が Claude Code の期待する形式と一致していないことが原因のよう。
- protocolVersion が必須だが undefined
- serverInfo が必須だが undefined
以下に書き換える
func (s *WeatherMCPServer) handleInitialize(req JSONRPCRequest) JSONRPCResponse { // MCPプロトコルに準拠した正しい形式 result := map[string]interface{}{ "protocolVersion": "2024-11-05", "serverInfo": map[string]interface{}{ "name": s.name, "version": s.version, }, "capabilities": map[string]interface{}{ "tools": map[string]interface{}{}, }, } return JSONRPCResponse{ JSONRPC: "2.0", ID: req.ID, Result: result, } }
これで
claude
で実行ができた!
詳しいgoのコードの中身は別で確認してみます。
とりあえず、goでも動かせそうなことがわかった。
pythonのライブラリじゃなくても動かせることで、mcpサーバーのツールの作成のためにはインターフェースとして何が必要なのかが把握できるようになった。
あとはmcpサーバーのlocal configの保存場所等を探っていったりしていきます。
MCPサーバーのチュートリアル動かしてみる
今更ながらMCPサーバーについて手を動かしながら理解してみる。
podcastなどでMCPサーバーについてのあれこれは色々と話題に出ていたので聞いては見たけど、正味よくわかっていなかったので、実際に触って腑に落ちるようにしてみる。
ちゃんとやって見る前の理解
MCPサーバーに自然言語でクエリを投げることで、そのMCPサーバーがAPIの使い方リストみたいなものを参照して適切なAPIを呼び出してくれる? みたいなざっくり感。 よく例に出されるのは天気予報のサービスをAPIから取ってきて回答してくれる?みたいな。なんかたとえが悪いのか、しっくりきていない。
公式チュートリアル
https://docs.anthropic.com/ja/docs/claude-code/mcp
日本語訳がある、ありがたい
まずはMCPとは?というドキュメントを読み込んでみる。概要だけ。 https://modelcontextprotocol.io/introduction

claudeやIDEが複数のMCPサーバーを通じてローカルや外部APIなどのデータソースにアクセスして、データをいい感じにする
公式チュートリアルはなんか登場人物が多くてその前段がまだ理解できていない気がするので、ドキュメントの方のチュートリアルを動かしてみる。 天気予報とかのやつ。
https://modelcontextprotocol.io/introduction
こっちの方
サーバー開発者向けクイックスタート
uvのinstall
pythonの環境構築。 今どきはuvなのか。聞いたことはあったけど、使ったことがなかった。
curl -LsSf https://astral.sh/uv/install.sh | sh
curl -LsSf https://astral.sh/uv/install.sh | sh
downloading uv 0.7.17 aarch64-apple-darwin
no checksums to verify
installing to /Users/yoshii/.local/bin
uv
uvx
everything's installed!
To add $HOME/.local/bin to your PATH, either restart your shell or run:
source $HOME/.local/bin/env (sh, bash, zsh)
source $HOME/.local/bin/env.fish (fish)
source ~/.zshrc which uv
installできていた。
環境セットアップ
# Create a new directory for our project uv init weather cd weather # Create virtual environment and activate it uv venv source .venv/bin/activate # Install dependencies uv add "mcp[cli]" httpx # Create our server file touch weather.py
httpxっていうパッケージがあるんだ
requestsより高機能らしい(GPT談)
コード
from typing import Any import httpx from mcp.server.fastmcp import FastMCP # Initialize FastMCP server mcp = FastMCP("weather") # Constants NWS_API_BASE = "https://api.weather.gov" USER_AGENT = "weather-app/1.0" async def make_nws_request(url: str) -> dict[str, Any] | None: """Make a request to the NWS API with proper error handling.""" headers = { "User-Agent": USER_AGENT, "Accept": "application/geo+json" } async with httpx.AsyncClient() as client: try: response = await client.get(url, headers=headers, timeout=30.0) response.raise_for_status() return response.json() except Exception: return None def format_alert(feature: dict) -> str: """Format an alert feature into a readable string.""" props = feature["properties"] return f""" Event: {props.get('event', 'Unknown')} Area: {props.get('areaDesc', 'Unknown')} Severity: {props.get('severity', 'Unknown')} Description: {props.get('description', 'No description available')} Instructions: {props.get('instruction', 'No specific instructions provided')} """ @mcp.tool() async def get_alerts(state: str) -> str: """Get weather alerts for a US state. Args: state: Two-letter US state code (e.g. CA, NY) """ url = f"{NWS_API_BASE}/alerts/active/area/{state}" data = await make_nws_request(url) if not data or "features" not in data: return "Unable to fetch alerts or no alerts found." if not data["features"]: return "No active alerts for this state." alerts = [format_alert(feature) for feature in data["features"]] return "\n---\n".join(alerts) @mcp.tool() async def get_forecast(latitude: float, longitude: float) -> str: """Get weather forecast for a location. Args: latitude: Latitude of the location longitude: Longitude of the location """ # First get the forecast grid endpoint points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" points_data = await make_nws_request(points_url) if not points_data: return "Unable to fetch forecast data for this location." # Get the forecast URL from the points response forecast_url = points_data["properties"]["forecast"] forecast_data = await make_nws_request(forecast_url) if not forecast_data: return "Unable to fetch detailed forecast." # Format the periods into a readable forecast periods = forecast_data["properties"]["periods"] forecasts = [] for period in periods[:5]: # Only show next 5 periods forecast = f""" {period['name']}: Temperature: {period['temperature']}°{period['temperatureUnit']} Wind: {period['windSpeed']} {period['windDirection']} Forecast: {period['detailedForecast']} """ forecasts.append(forecast) return "\n---\n".join(forecasts) if __name__ == "__main__": # Initialize and run the server mcp.run(transport='stdio')
諸々のコードをその通りに書いて実行
$ uv run weather.py
uv run weather.py
Traceback (most recent call last):
File "/Users/yoshii/src/github.com/my0shym/mcp-practice-202507/weather/weather.py", line 3, in <module>
from mcp.server.fastmcp import FastMCP
File "/Users/yoshii/src/github.com/my0shym/mcp-practice-202507/weather/.venv/lib/python3.10/site-packages/mcp/__init__.py", line 1, in <module>
from .client.session import ClientSession
File "/Users/yoshii/src/github.com/my0shym/mcp-practice-202507/weather/.venv/lib/python3.10/site-packages/mcp/client/session.py", line 5, in <module>
import anyio.lowlevel
File "/Users/yoshii/src/github.com/my0shym/mcp-practice-202507/weather/.venv/lib/python3.10/site-packages/anyio/__init__.py", line 26, in <module>
from ._core._sockets import connect_tcp as connect_tcp
File "/Users/yoshii/src/github.com/my0shym/mcp-practice-202507/weather/.venv/lib/python3.10/site-packages/anyio/_core/_sockets.py", line 6, in <module>
import ssl
File "/Users/yoshii/.anyenv/envs/pyenv/versions/3.10.1/lib/python3.10/ssl.py", line 98, in <module>
import _ssl # if we can't import it, let the error propagate
ImportError: dlopen(/Users/yoshii/.anyenv/envs/pyenv/versions/3.10.1/lib/python3.10/lib-dynload/_ssl.cpython-310-darwin.so, 0x0002): Library not loaded: /opt/homebrew/opt/openssl@1.1/lib/libssl.1.1.dylib
Referenced from: <EBE36D48-59BA-3105-BBFA-BAA22CBDF75F> /Users/yoshii/.anyenv/envs/pyenv/versions/3.10.1/lib/python3.10/lib-dynload/_ssl.cpython-310-darwin.so
Reason: tried: '/opt/homebrew/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/opt/homebrew/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/usr/local/lib/libssl.1.1.dylib' (no such file), '/usr/lib/libssl.1.1.dylib' (no such file, not in dyld cache)
(weather) weather <feature/init>%
エラー
pyenvでpython 3.10.1を使用していたが、これが古すぎてopenssl周りで不具合があった模様。
pyenv versionsで確認
3.10.7が入っていたので、そっちを使用する。
.python-versionを書き換え
一度.venvを削除し、再度設定。
$ uv run weather.py
これもだめ。
pyenvでinstallした3.10.7も入れ直さないとだめっぽい。
なんでpythonってこんなライブラリ周りで毎回躓くんだろ
openssl周り。めんどいな。
pyenvの3.10.7を再install
env \ LDFLAGS="-L/opt/homebrew/opt/openssl@3/lib" \ CPPFLAGS="-I/opt/homebrew/opt/openssl@3/include" \ PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@3/lib/pkgconfig" \ pyenv install --force 3.10.7
pyenv local 3.10.7 # プロジェクトディレクトリで実行
古い仮想環境を削除して作り直す
rm -rf .venv uv venv
依存を再インストール
uv add "mcp[cli]" httpx
確認:OpenSSL のバージョン
source .venv/bin/activate python -c "import ssl; print(ssl.OPENSSL_VERSION)" OpenSSL 3.5.0 8 Apr 2025
3系が表示されているのでOK.
サーバー起動
$ uv run weather.py
ようやく実行できた。
Claude for Desktopでサーバーをテストする
設定ファイルを編集
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
{ "mcpServers": { "weather": { "command": "/Users/yoshii/.local/bin/uv", "args": [ "--directory", "/Users/yoshii/src/github.com/my0shym/mcp-practice-202507/weather", "run", "weather.py" ] } } }
ツールが追加されていることが確認できた。

質問してみる。
確認が出てくる。
ちゃんと定義したツールを使っていそう。

結果が取得できた。

claude for DesktopからMCPサーバーを通じて外部の天気情報を取得できた。
MCPサーバーの雰囲気は掴めたけど、実用的に取り入れるには色々調査したり触ってみる必要がありそう。
- claude for Desktop以外だとどのツールから呼び出せるのか
- サーバーはローカル起動する必要があるのか
- mcpサーバーが返すデータ形式は何が適切なのか
- claude側でプロンプトから適切なMCPツールを選択するためのMCPサーバーの作り方及びプロンプトのやり方
このあたりは確認してみたい。
golangのerrorとlogについて整理してみる備忘録
雑に動かす
エラー周り、ログ周りは雰囲気でやっていたので、このあたりでちゃんと整理して理解したいなと思います。
とりあえず雑に書いて動かしてみます。
標準のerrosとlog/slogを使ってやってみます。
package main import ( "errors" "fmt" "log/slog" ) var ErrDivisionByZero = errors.New("division by zero") var ErrNegativeDivisor = errors.New("negative divisor") func divide(a int, b int) (*int, error) { if b == 0 { return nil, ErrDivisionByZero } if b < 0 { return nil, fmt.Errorf("err: %w, arg1: %d, arg2: %d", ErrNegativeDivisor, a, b) } res := a / b return &res, nil } func main() { res1, _ := divide(10, 2) fmt.Println(*res1) // 5 _, err := divide(10, 0) if err != nil { if errors.Is(err, ErrDivisionByZero) { slog.Error(ErrDivisionByZero.Error()) // division by zero } } _, err = divide(10, -1) if err != nil { slog.Error("unexpected Error", "err", err.Error()) // unexpected Error } }
出力はこんな感じ。
go run main.go 5 2024/11/16 16:00:13 ERROR division by zero 2024/11/16 16:00:13 ERROR unexpected Error err="err: negative divisor, arg1: 10, arg2: -1"
これをベースにして、色々と拡張して検証をしていきたいと思います。
確認していきたい内容
Go言語初心者がハマるGo言語の細かい文法などの気付き
随時更新
goではintの計算結果は、intになる。
なので、
<int>10 / <int>100
の計算結果は、0になる。
切り捨てか切り上げか四捨五入かは、調べてない。
なので、予めfloat32とかにcastしておく必要がある。
ポインタは変数に入れないと使用できないケースがある
ポインタ型を渡したいときに
hoge := "hoge" // string型の変数hogeに代入できます fmt.Println(hoge) p := &hoge // 変数hogeのポインタ(アドレス)を取得できます fmt.Println(p) // 変数hogeのポインタ(アドレス)を表示できます fmt.Println(*p) // 変数hogeのポインタ(アドレス)の中に入っている値を表示できます p2 := &"hoge" // これで値"hoge"が入っている値をセットしたいところですが、エラーになります。 fmt.Println(p2)
ポインタは、変数のアドレスを取得できるやつなので、一度変数に入れないとだめです。
githubのhttpsのcloneで`repository not found`のエラー
$ git clone https://github.com/my0shym/myproject
でrepository not found
のエラーが発生
今まではパスワードが聞かれていたはずなのだが、それもなくエラーになっている。
以前パスワードを聞かれて入力したらエラーになったことはあったが、それはパスワードではなくtokenを入力する必要があるように変わったことが原因だった。
しかし、今回はそもそもパスワード(トークン)すら聞かれない。
調べてみると、どうやらmacではbrewでinstallしたgitではkeychainにパスワードが初回に保存されていて、毎回入力しなくて良いようにそこから認証を通そうとしているとのことだった。
これも設定で変更できるのだが。
osxkeychainなるオプションを指定すれば平文でパスワードを保存することもなく、便利に使えそうだった。
$git config --global credential.helper osxkeychain
を実行しても何も変化がなく(ここでパスワード(token)を設定するのかと思った)、依然として動かない。
色々試行錯誤してみたところ、解決。
結論としては、自分のmacに一番最初に設定したのが別のgithubアカウントのtokenだったことが原因でした。
複数アカウントをsshとかで使っちゃうと運用が面倒くさくなると思ったので、そっちのアカウントはsshじゃなくhttpsで、本アカウントをsshとして、サブアカウントで毎回httpsで操作しようとするときはtoken入力すればいいやくらいに思っていて特に棲み分けは考えなくてもいいやと思っていたところ、実は裏側でtokenが勝手に設定されていてそれを使いまわそうとされていたのでした〜
このやり方でパスワードを再設定したら、いけました!
しかし、となるとサブ垢で使っているgithubアカウントでまたhttps操作をしようとすると同じ問題が発生するので、これをどう対処するか。。という問題は残っていますね。
それはまた直面したときに。
たぶんそもそもhttpsを使わずに、configで分けておくのが良いのだと思っているのだがー。メモ。
それを言い出すと、そもそもgithubアカウントの複数運用自体が、無駄な工数という説もある。メモ。
goのginをdocker-composeでローカル環境で動かす
以下のような構成で作成しました。
main.go作成後、以下を実行します。
ファイルの内容はこの通り
Dockerfile
main.go
docker-compose.yml
ハマったところとしては、ginのチュートリアルで
router.Run("localhost:8080")
としてサーバーを動かしていたのでそのままやっていたら、コンテナ内で
をやっている分にはレスポンスが返ってきていたのですが、ホストからアクセスしようとすると、そもそもの疎通ができていない状態になってしまっていました。
他の記事等を見ているとlocalhostなしでRun(:8080)としているコードが多かったのでやってみたらこれでいけました。
うーんしかし根本の原因はちょっとよくわからなかったです。
コンテナ力がちょっと足りていないなあ。精進します。
あと微妙にハマったのは、ローカルmacでgo mod tidyでgo.sumを生成していたのですが、ローカルで使っていたgoのバージョンとDockerfileで使っていたgoのイメージのバージョンが違っていたがために、go.sumが整合性取れなくなっていて動かなかった問題もありました。
本当はalpine使ったほうが良かったのかもしれないですが、動いているので大目に見てもらいます。
ともあれ、これで動きました!
ここから開発にブーストかけていきたいと思います!
Go言語でCloud SQLとCloud Runを連携する奮闘記
接続しているっぽいところまでは割と簡単に進めましたが、本当に接続している?っていう確認に少し手間がかかりそうな気がしました。
動作確認の手順として
1. goでDBに接続してレコードを取ってきて表示するようなコードを書く
2. 1をイメージとしてArtifact Registryにpush
3. Cloud SQLのインスタンスを作成する、データを作っておく
4. Artifact RegistryからCloud Runサービスをデプロイ(Cloud SQLに接続する設定)
5. Cloud RunがDBからレコードを取得できていることをブラウザから確認
という手順で進めていきたいです。
cloud sql自体の作成やデータアクセスはこちらから確認できますが、他のリソースからアクセスをさせたいです。
ローカルからアクセスする場合には、プロキシなるものを用意しないといけないっぽい。
でもGCP内でやり取りするなら、たぶんそんなのはいらない気がする。
これやったら動きそうな気がしてるけど、goのコードが長い。。
もう少しシンプルに接続だけを確認したい。
これは甘えか?
普通にこれでいいのでは?
接続方法は書いてありそうだけど、goのアプリケーションで、どうやって使うのかがわからないな。
これはgoの知識不足か。
普通にレコードselectができればいいんですが。
アプリケーションでの使用も書いてあるが、ちと情報量多いな。
シンプルに、コネクションを作った後の動きは別でgoのsqlパッケージの使い方として調べます。
これを実装してみるか。
ローカルのmysqlと接続してみることにする。
いや、これの例の通りmysqlもdockerで立ち上げることにする。
とすると、エラーになりました。
本質的な解決策じゃないようだが、とりあえず問題は解決するとのこと。
のようにplatform: linux/x86_64を追加してdocker compose upすると、うまく立ち上がった。
で接続後、以下実行
最終的には以下のdocker-compose.yml
これに対して、接続するようなgoのコードを作る。
main.go
goの外部パッケージ追加方法は、まずmain.goと同じディレクトリ内で
で始めてから、
main.goにコードを書いた状態で、
をする。
すると、その中でimportしているがmodに追加されていないものを自動でimportしてくれる。
これで疎通確認できました!
これでとりあえず、mysqlからデータを取得するgoのコードが確認できました。
中身も見てみて、なるほどねって感じです。Goライクな書き方にだんだんと慣れてきている気がします。
これをそのままCloud Runにデプロイしようと思います。
Dockerfileは公式より、
これをベースにして、
まずは疎通確認のため、goのコードもこのサンプルのまま使います。
main.go
タグを付けてイメージをbuildする
Artifact Registryにpushする
Artifact Registryにimageがpushされているのを確認したら、コンソールからCloud Runサービスをデプロイ
おっと、ここでエラー。
これは以前解決したエラーでした。
ここで解決していますが、buildコマンドを以下に変更です。
再度pushして、Artifact RegistryからCloud Runにデプロイします。
公開されているURLからアクセスすると、
Hello, Docker! <3
の文字が表示されていました。
これでDockerfileが正しく動くことが確認できました。
これを次は、先程のDB接続用のmain.goに変更してpushします。
本当はDB接続情報は環境変数にセットして使用したいところですが、一旦決め打ちで作って動作を確認してから環境変数化していきたいと思います。
と思ったけど、このままだと、サーバーとしての機能は持たせていないからhttpリクエスト送ってもだめだ。
まずサーバーとしての機能は維持しつつ、handefuncでmysqlからデータを取得するように組み合わせる必要があるな。
Dockerfileのサンプルコードを使ってもいいんだけど、echoのモジュールを使ってリクエストの処理をしていたので、これは使わないで、シンプルにnet/httpの標準モジュールを使っていく。
以下より、まずはシンプルなwebサーバーとして動かす。
すごい回り道してきたきがするなぁ。
これをpushしてArtifactからCloud Runのサービスデプロイ。
→ URLアクセスでHello, Worldが表示された!
これのHandleFuncにmysqlからデータを取ってくる関数を追加しよう。
これでデプロイすると、
/ ではHello, worldが表示されるが、
/sql-man ではService Unavailable と表示される。
これはcloud sqlをまだ設定していないので正しい。
ここからcloud sqlの設定を入れていく。
と、公式のつなぎ込み方を再度確認すると、自分のコードではtcpで疎通している部分がunixソケットで疎通している。
unixソケットについて整理します。
・UNIX ドメインソケットもソケットもいずれもプロセス間のデータのやり取りを行うための手法の一つ
・UNIX ドメインソケットではプロセス間通信にファイルシステムを利用する(拡張子 .sock という場合が多い)その為、 同じホストでのプロセス間通信 として利用される
・ソケット通信は 異なるホスト で TCP や UDP を使ってプロセス間のやり取りが可能
Unix socket connectionってので通信しているんだが、これはソケット通信なのか、unixドメインソケット通信なのかどっちなんだ?
そこまでは把握する必要はないか?
形式的には
/cloudsql/project:region:instance
みたいな感じだから、異なるホストっていう扱いなのかな?
このあたりは宿題として放置。まず動くことを目標とします。
元々Cloud SQLのインスタンスは作成済みだったので、それを使用します。
以下コードで疎通の確認ができた!
これを一応環境変数を読み取るように変更する。
ここらへんは問題なく進めるはず。
→コメントアウト部分を復活させたものをArtifactにpushしてからCloud Runにデプロイ、その際に環境変数で
DB_USER
DB_PASS
DB_NAME
INSTANCE_UNIX_SOCKET
を設定したら、いけました!
くぅ〜これにて完結です。
本当は上の4つの値はシークレットマネージャーで管理したほうが良さそうなんだけども、それはまたのお話ということで。
どっかできれいにしてまとめたいです。