🧊

OXCのASTに詳しいMCPサーバーを作った

Rustの勉強がてらで。

ローカルで動けばいい雑なMCPサーバーの実装自体はやったことあるけど、JS/TSで書けるなら、Rustでもできるはず。

作ったもの

leaysgur/oxc-ast-mcp https://github.com/leaysgur/oxc-ast-mcp/

OXCのASTがどういう風になってるか、どういうものがあり、どんな構造になってるのかを知る補助となるツール。

また、コード片を渡してASTにしたり、それが生成されたコードなら、validなシンタックスがどうかチェックできるというやつ。

地味に便利なはず。

RustのSDK探し

さっと調べた限り、この2つがあった。

前者のほうが公式っぽさはあるけど、サンプルをビルドしてみたら、最新のClaudeCodeからはtoolsが認識できなかった。

原因はよくわからんけど、MCPの実装とClaudeCodeの実装で何か問題があるのであろう・・・。

No tools available on Claude desktop · Issue #326 · modelcontextprotocol/rust-sdk https://github.com/modelcontextprotocol/rust-sdk/issues/326

たぶんこれと同じ状況だと思われるけど、ちゃんとは調べてない。

rust-mcp-sdkは、最新の2025-06-18のSpecにも対応してるしちゃんと動作するようだったので、こっちを使うことにした。

コード

そもそも要件もミニマムなので、大して詰まるところもなかった。リポジトリ参照。

いちおうメイン。

#[tokio::main]
async fn main() -> SdkResult<()> {
    let server_details = InitializeResult {
        server_info: Implementation {
            name: "OXC AST MCP Server".to_string(),
            version: "0.1.0".to_string(),
            title: None,
        },
        capabilities: ServerCapabilities {
            tools: Some(ServerCapabilitiesTools { list_changed: None }),
            ..Default::default()
        },
        meta: None,
        instructions: Some("This server helps you to understand how ASTs are structured by OXC parser. Also useful to quick check for your code which should be valid syntax.".to_string()),
        protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
    };

    let server = server_runtime::create_server(
        server_details,
        StdioTransport::new(TransportOptions::default())?,
        MyServerHandler,
    );
    server.start().await
}

あとは、create_server(details, transport, handler)handlerで、ServerHandler traitを実装しておくだけ。

struct MyServerHandler;

#[async_trait]
impl ServerHandler for MyServerHandler {
    async fn handle_list_tools_request(
        &self,
        _request: ListToolsRequest,
        _runtime: &dyn McpServer,
    ) -> std::result::Result<ListToolsResult, RpcError> {
        Ok(ListToolsResult {
            meta: None,
            next_cursor: None,
            tools: MyTools::tools(),
        })
    }

    async fn handle_call_tool_request(
        &self,
        request: CallToolRequest,
        _runtime: &dyn McpServer,
    ) -> std::result::Result<CallToolResult, CallToolError> {
        let tool_params = MyTools::try_from(request.params).map_err(CallToolError::new)?;

        match tool_params {
            MyTools::DocsTool(docs_tool) => docs_tool.call(),
            MyTools::ParseTool(parse_tool) => parse_tool.call(),
            MyTools::CheckTool(check_tool) => check_tool.call(),
        }
    }
}

よしなにtoolsを呼び分けるくらいしかやることない。

#[async_trait]は、asyncな関数をtraitとして実装する場合に必要とのこと。

why async fn in traits are hard · baby steps https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/

あとはOXCを使っていつものコードを書いてるだけ。

docs toolで返すための元ネタを生成するのが一番大変だった。

特定のcrateで公開されてるstructとenumを列挙する | Memory ice cubes https://leaysgur.github.io/posts/2025/08/01/095617/

まとめ

RustでもMCPは簡単に実装できる。(少なくとも、ローカルでstdioで使う程度の用途なら) というかただのJSON-RPCサーバーなので、何も難しいことはなかった。

まぁ作ってみたはいいものの、AI関連のツールは、実装時の設計やデザインもさることながら、実装後の評価が一番難しいよな〜って。