Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"mcpServers": {
"mountix-mcp-development": {
"url": "http://127.0.0.1:8080/mcp"
}
}
}
23 changes: 22 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Mountixは、クリーンアーキテクチャパターンを使用してRustで
- `DATABASE_URL`にMongoDB接続文字列
- `DATABASE_NAME=mountix_db`
- サーバーのホスト/ポート設定
- MCPサーバー設定(任意):
- `MCP_STATEFUL_MODE`(既定: `false`)
- `MCP_JSON_RESPONSE`(既定: `true`)
- `MCP_ALLOWED_HOSTS`(カンマ区切り。未設定時は `HOST` の値を含む既定値が使われる)

### データベースセットアップ

Expand All @@ -48,7 +52,8 @@ cd ../
- Axum Webフレームワークのセットアップとルーティング
- HTTPハンドラーとJSONシリアライゼーション
- エントリーポイント: `startup/mod.rs`にサーバー設定
- ルート: `/api/v1/mountains`, `/api/v1/hc` (ヘルスチェック)
- ルート: `/api/v1/mountains`, `/api/v1/hc` (ヘルスチェック), `/mcp` (MCPサーバー)
- `rmcp` の `StreamableHttpService` を `nest_service` で `/mcp` にマウントし、MCPサーバーを公開(実装は `mcp/` 配下)

2. **mountix-app** (Use Case/Application)
- ビジネスロジックとアプリケーションワークフロー
Expand Down Expand Up @@ -78,6 +83,8 @@ cd ../
- **ログ**: tracingによるJSON出力
- **環境**: dotenvy(.envファイル読み込み)
- **CORS**: API アクセス用に設定済み
- **MCP**: `rmcp` クレート(Streamable HTTP transport)によるMCPサーバー
- **JSON Schema**: `schemars`(MCPツールのパラメータスキーマ生成)

## APIエンドポイント

Expand All @@ -87,6 +94,20 @@ cd ../
- `GET /api/v1/mountains/geosearch` - 地理的検索
- `GET /api/v1/hc` - ヘルスチェック
- `GET /api/v1/hc/mongo` - MongoDBヘルスチェック
- `POST /mcp` - MCP(Model Context Protocol)Streamable HTTP エンドポイント

## MCPサーバー

`mountix-driver` 配下に Model Context Protocol サーバーを実装し、Cursor などの MCP 対応クライアントから山岳データを操作できるようにしています。

### 提供ツール

すべて `mountix-app` のユースケース(`mountain_use_case` / `surrounding_mountain_use_case`)を呼び出し、REST API と同じ JSON モデル(`JsonMountain` / `JsonMountainsResponse` / `JsonBoxMountainsResponse` / `JsonSurroundingMountainResponse`)を文字列として返します。

- `get_mountain` - 山岳IDで山岳情報を1件取得
- `find_mountains` - 名称・都道府県・タグなどで山岳検索
- `find_mountains_by_box` - 緯度経度の矩形範囲で山岳検索
- `find_surroundings` - 指定した山岳の周辺山岳を検索

## 開発ノート

Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ members = [

[workspace.package]
name = "mountix"
version = "1.2.0"
version = "1.3.0"
edition = "2021"

[workspace.dependencies]
Expand All @@ -31,3 +31,5 @@ tokio-test = "0.4.4"
mockall = "0.13.0"
hyper = "1.6.0"
serde_json = "1.0.140"
rmcp = { version = "1.7", features = ["server", "macros", "transport-streamable-http-server"] }
schemars = "1.0"
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ MOUNTAINS_URL=http://127.0.0.1:8080/api/v1/mountains
DOCUMENTS_URL=http://127.0.0.1:3000
DEFAULT_DISTANCE=5000
MAX_DISTANCE=100000

# MCP (Model Context Protocol) remote server settings (optional)
# MCP_STATEFUL_MODE=false
# MCP_JSON_RESPONSE=true
# MCP_ALLOWED_HOSTS=localhost,127.0.0.1,127.0.0.1:8080
```

Execute `cargo run` command.
Expand All @@ -56,6 +61,70 @@ Execute `cargo run` command.
cargo run
```

## REST API

REST API は Axum で実装されており、JSON 形式で山岳データを提供します。

詳細なリクエスト/レスポンス仕様は [Postman Public API Network](#postman-public-api-network) を参照してください。

### エンドポイント

| メソッド | パス | 概要 |
| --- | --- | --- |
| GET | `/api/v1/` | API の基本情報(`MOUNTAINS_URL` / `DOCUMENTS_URL` など) |
| GET | `/api/v1/mountains` | 条件指定の山岳一覧取得 |
| GET | `/api/v1/mountains/{id}` | 山岳IDで1件取得 |
| GET | `/api/v1/mountains/{id}/surroundings` | 指定した山岳の周辺山岳を取得 |
| GET | `/api/v1/mountains/geosearch` | 緯度経度の矩形範囲で山岳を検索 |
| GET | `/api/v1/hc` | ヘルスチェック |
| GET | `/api/v1/hc/mongo` | MongoDB ヘルスチェック |

### `/api/v1/mountains` の主なクエリパラメータ

| パラメータ | 説明 |
| --- | --- |
| `name` | 山岳名(部分一致) |
| `prefecture` | 都道府県ID |
| `tag` | タグID |
| `offset` | 取得開始位置 |
| `limit` | 取得件数 |
| `sort` | ソート条件 |

### レスポンス形式

成功時は `JsonMountain` / `JsonMountainsResponse` などの JSON、エラー時は `JsonErrorResponse`(`messages` 配列を含む)を返します。MCP ツールも同じスキーマの文字列を返します。

## MCP (Model Context Protocol) Server

[Model Context Protocol](https://modelcontextprotocol.io/) の Streamable HTTP transport によるエンドポイントも提供しています。

Cursor などの MCP 対応クライアントから、山岳データを検索・取得するツールを呼び出すことができます。

### エンドポイント

- `POST /mcp` - MCP Streamable HTTP エンドポイント

### 提供ツール

| ツール名 | 説明 |
| --- | --- |
| `get_mountain` | 山岳IDを指定して山岳情報を1件取得 |
| `find_mountains` | 山岳名・都道府県・タグなどの条件で山岳を検索 |
| `find_mountains_by_box` | 緯度経度の矩形範囲を指定して山岳を検索 |
| `find_surroundings` | 指定した山岳の周辺にある山岳を検索 |

レスポンスは REST API と同じスキーマ(`JsonMountain` など)の JSON 文字列として返されます。

### サーバー設定(環境変数)

`.env` で MCP サーバーの挙動を調整できます(いずれも任意)。

| 環境変数 | デフォルト | 説明 |
| --- | --- | --- |
| `MCP_STATEFUL_MODE` | `false` | Streamable HTTP のセッション管理を有効化するか |
| `MCP_JSON_RESPONSE` | `true` | レスポンスを JSON で返すか(`false` で SSE) |
| `MCP_ALLOWED_HOSTS` | `localhost,127.0.0.1,::1,$HOST` | 許可する `Host` ヘッダ。カンマ区切りで指定。未設定の場合は `HOST` 環境変数の値を含む既定値が使われる |

## Postman Public API Network

Postman Public API Network で API を公開しています。
Expand All @@ -68,6 +137,7 @@ Postman Public API Network で API を公開しています。
- ルーターとサーバーの起動を実装する
- Axum の機能を利用してエンドポイントとサーバーの起動までを実装する
- 内部的に行われた処理の結果、どのようなステータスコードを返すかをハンドリングしたり、JSON のシリアライズ・デシリアライズも担当する
- `rmcp` クレートを利用した MCP サーバー(`/mcp` エンドポイント)の公開もこのレイヤーで実装する
- mountix-app (app or usecase)
- ユースケースのレイヤーで、アプリケーションを動作させるために必要なロジックを記述する
- 複数リポジトリをまたいでアプリケーションに必要なデータ構造を返すなどをおこなう
Expand Down
3 changes: 3 additions & 0 deletions mountix-driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ tracing-subscriber = { workspace = true }
dotenvy = { workspace = true }
tower = { workspace = true }
tower-http = { workspace = true }
rmcp = { workspace = true }
schemars = { workspace = true }
serde_json = { workspace = true }

[dev-dependencies]
tokio-test = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions mountix-driver/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod mcp;
pub mod model;
pub mod module;
pub mod routes;
Expand Down
73 changes: 73 additions & 0 deletions mountix-driver/src/mcp/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
pub mod server;

use crate::mcp::server::MountixMcpServer;
use crate::module::Modules;
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager, StreamableHttpServerConfig, StreamableHttpService,
};
use std::env;
use std::sync::Arc;

pub fn mcp_service(
modules: Arc<Modules>,
) -> StreamableHttpService<MountixMcpServer, LocalSessionManager> {
let modules_for_factory = modules.clone();
let config = mcp_config();

StreamableHttpService::new(
move || Ok(MountixMcpServer::new(modules_for_factory.clone())),
LocalSessionManager::default().into(),
config,
)
}

fn mcp_config() -> StreamableHttpServerConfig {
let mut config = StreamableHttpServerConfig::default()
.with_stateful_mode(parse_bool_env("MCP_STATEFUL_MODE", false))
.with_json_response(parse_bool_env("MCP_JSON_RESPONSE", true));

if let Ok(hosts) = env::var("MCP_ALLOWED_HOSTS") {
let allowed_hosts: Vec<String> = hosts
.split(',')
.map(str::trim)
.filter(|host| !host.is_empty())
.map(str::to_string)
.collect();
if !allowed_hosts.is_empty() {
config = config.with_allowed_hosts(allowed_hosts);
}
} else if let Ok(host) = env::var("HOST") {
config = config.with_allowed_hosts(["localhost", "127.0.0.1", "::1", host.as_str()]);
}

config
}

fn parse_bool_env(key: &str, default: bool) -> bool {
env::var(key)
.ok()
.and_then(|value| match value.to_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
})
.unwrap_or(default)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_bool_env() {
std::env::set_var("TEST_BOOL_TRUE", "true");
std::env::set_var("TEST_BOOL_FALSE", "false");

assert!(parse_bool_env("TEST_BOOL_TRUE", false));
assert!(!parse_bool_env("TEST_BOOL_FALSE", true));
assert!(parse_bool_env("UNDEFINED_BOOL", true));

std::env::remove_var("TEST_BOOL_TRUE");
std::env::remove_var("TEST_BOOL_FALSE");
}
}
Loading
Loading