PlantUMLコードを検証するMCPサーバーを実装してみた

# 概要
visual studio codeでエージェントモードが提供されたため、お試しでMCPサーバーを作成してみました。

[GitHub Copilotでバイブコーディング：エージェントモードとMCPサポートがVS Codeユーザーに提供開始](https://github.blog/jp/2025-04-07-github-copilot-agent-mode-activated/)

作ったのはPlantUMLコードを検証するMCPサーバーです。
LLMはPlantUMLコードを生成してくれるんですが、実際に画像に変換する際にエラーがでることが多かったからです。

通信方式は[Server-Sent Events (SSE)](https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse)を採用しました。

動作イメージは以下な感じです。
```plantuml
left to right direction
!include <logos/visual-studio-code.puml>
!include <cloudogu/dogus/plantuml.puml>

title "Project Overview"
package "Docker Compose" {
  node myplantuml as "PlantUML Server" <<$plantuml>> {
  }
  note right of myplantuml
    Remote Service
  end note
  node plantuml_mcp_server as "PlantUML MCP Server" <<container>> {
  }
  note right of plantuml_mcp_server
    MCP Server
  end note
}
plantuml_mcp_server <-> myplantuml : Communicates via port 8080
actor user
node vscode as "github copilot" <<$visual-studio-code>>
note right of vscode
  Host with MCP Client
end note
user --> vscode
vscode <--> plantuml_mcp_server: Communicates via port 3000
user --> myplantuml: Communicates via port 8080

```


<details><summary>docker版増やしました。</summary>

参考: [Dockerizing your .NET C# MCP Server for AI Clients like Claude Desktop](https://laurentkempe.com/2025/03/27/dockerizing-your-dotnet-csharp-mcp-server-for-ai-clients-like-claude-desktop/)


```plantuml
left to right direction
!include <logos/visual-studio-code.puml>
!include <cloudogu/dogus/plantuml.puml>

title "Project Overview"
  node myplantuml as "PlantUML Server" <<$plantuml>> {
  }
  note bottom of myplantuml
    Remote Service
  end note

package "Docker" {
  node plantuml_mcp_server as "PlantUML MCP Server" <<container>> {
  }
  note top of plantuml_mcp_server
    MCP Server
  end note
}
plantuml_mcp_server <-> myplantuml : Communicates
actor user
node vscode as "github copilot" <<$visual-studio-code>>
note bottom of vscode
  Host with MCP Client
end note
user --> vscode
vscode <--> plantuml_mcp_server: docker run
```

イメージ作成
```bash
cd plantuml-mcp-server-stdio
dotnet publish /t:PublishContainer
```

host設定例
```json: settings.json
    "mcp": {
        "servers": {
            "my-plantuml-mcp-server-docker": {
                "type": "stdio",
                "command": "docker",
                "args": [
                    "run",
                    "--rm",
                    "-i",
                    "--network=host",
                    "plantuml-mcp-server-stdio",
                    "PlantumlBaseUrl=http://your_plantuml_server/"
                ],
            },
        }
    }
```

</details>



[MCP サーバーを自作して GitHub Copilot の Agent に可読性の低いクラス名を作ってもらう](https://zenn.dev/microsoft/articles/semantickernel-mcp4)
が大変参考になりました。ありがとうございました。

# 実装
* [plantuml-validator-mcp-server](https://github.com/kwhrkzk/plantuml-validator-mcp-server/tree/main)


# 使用した言語、ライブラリ、ツール
* C#
* .NET 9.0
* ModelContextProtocol.AspNetCore:0.1.0-preview.6
* docker
* github Copilot

# MCPサーバーの実装例
[Model Context Protocol (MCP)](https://github.com/modelcontextprotocol)のC#用SDKがあり、それを使用しました。

```c#: Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddCommandLine(args);

builder.Services.AddMcpServer()
    .WithToolsFromAssembly();

var app = builder.Build();

app.MapMcp();

app.Run("http://0.0.0.0:3000");
```

余計な処理もありますけど、これだけでMCPサーバーとして待ち受けます。
ツールについては後述します。

```C#
app.MapMcp();
```
でエンドポイントに割り付けています。
デフォルトは/sseです。今回でいうと
http://localhost:3000/sse
が設定するurlになります。

[src/ModelContextProtocol.AspNetCore/McpEndpointRouteBuilderExtensions.cs](https://github.com/modelcontextprotocol/csharp-sdk/blob/0c502c43af2b3bc2e3ca96e85a6057eed18e3677/src/ModelContextProtocol.AspNetCore/McpEndpointRouteBuilderExtensions.cs#L33)

に書かれています。
/messageというのもあるみたいです。

# Toolの実装例
MCPには[Tools](https://modelcontextprotocol.io/docs/concepts/tools)というコンセプトがあって、概要を機械翻訳すると

> LLMがサーバーを通してアクションを実行できるようにする
> 
> ツールはモデル・コンテキスト・プロトコル（MCP）の強力なプリミティブで、サーバーが実行可能な機能をクライアントに公開できるようにします。ツールを通じて、LLMは外部システムと対話し、計算を実行し、実世界でアクションを起こすことができます。


だそうです。
今回はToolを実装してみました。
他にも[Prompts](https://modelcontextprotocol.io/docs/concepts/prompts)や[Resources](https://modelcontextprotocol.io/docs/concepts/resources)があるみたいです。

```C#
namespace PlantUmlTools
{
    [McpServerToolType]
    public class PlantumlTool
    {
        [McpServerTool, Description("Validates the provided PlantUML message. If valid, returns 'Ok'. If invalid, returns detailed error information including error description, error line, and other metadata.")]
        public async Task<string> ValidatePlantUml(string message)
        {
            try
            {
                var encodedMessage = EncodePlantUml(message);
                Uri url = new (PlantUmlBaseUrl, $"txt/{encodedMessage}");
                using var httpClient = new HttpClient();

                var response = await httpClient.GetAsync(url);
                var responseContent = await response.Content.ReadAsStringAsync();

                var headers = response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value));

                var errorDescription = headers.ContainsKey("X-PlantUML-Diagram-Description") && headers["X-PlantUML-Diagram-Description"].Contains("Error");
                if (errorDescription == false)
                {
                    return "Ok";
                }

                var errorDetails = new StringBuilder();
                errorDetails.AppendLine($"FAIL");
                errorDetails.AppendLine($"{headers["X-PlantUML-Diagram-Description"]}");

                if (headers.ContainsKey("X-PlantUML-Diagram-Error"))
                {
                    errorDetails.AppendLine($"{headers["X-PlantUML-Diagram-Error"]}");
                }

                if (headers.ContainsKey("X-PlantUML-Diagram-Error-Line"))
                {
                    errorDetails.AppendLine($"Error-Line: {headers["X-PlantUML-Diagram-Error-Line"]}");
                }

                return errorDetails.ToString().Trim();
            }
            catch (Exception ex)
            {
                return ex.Message; // Return the exception message if an error occurs
            }
        }

    }
}
```

実装はとても簡単でした。
登録したいクラスにMcpServerToolType属性を付与して、McpServerTool属性を登録したいメソッドに付与するだけです。
Description属性に説明を加えるとLLMが理解するのに役立つ、気がします。。
後はやりたいことをゴリゴリ書くだけです。

# Toolの中身
以降はPlantUMLとのやりとりの話になります。
PlantUMLサーバーは異常時にはヘッダーにX-PlantUML-Diagram-ErrorとX-PlantUML-Diagram-Error-Lineを付与してくれます。(参考: [how to detect error in plantuml server rendering](https://forum.plantuml.net/6434/how-to-detect-error-in-plantuml-server-rendering?utm_source=chatgpt.com))


```plantuml
!include <cloudogu/dogus/plantuml.puml>
participant  plantuml_mcp_server
participant myplantuml <<$plantuml>>

plantuml_mcp_server -> myplantuml: GET /txt/{encodedMessage}
alt Valid response
    myplantuml --> plantuml_mcp_server: Response with validation result
    note right of plantuml_mcp_server
      Headers:
      - X-PlantUML-Diagram-Description: Valid diagram
    end note
else Error response
    myplantuml --> plantuml_mcp_server: Response with validation result
    note right of plantuml_mcp_server
      Headers:
      - X-PlantUML-Diagram-Description: Error details
      - X-PlantUML-Diagram-Error: Specific error message
      - X-PlantUML-Diagram-Error-Line: Line number of the error
    end note
end
```

* 正常時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/52b8f1c0-0bf1-48ca-9e50-7003a803e4a8.png)

* 異常時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/9f5eb5d6-5743-4bb1-b772-ab7218232149.png)

それを利用してLLMから引数としてplantumlコードをもらい、PlantUML Serverに投げてコードの検証をお願いしています。

正常の場合はOKだけ返して、異常の場合はヘッダーの内容を付与して返してます。

```plantuml
title ValidatePlantUml
start
:Receive PlantUML message;

:Encode the message;
:Construct URL for validation;

:Send HTTP GET request to the URL;
if (Response is successful?) then (yes)
    :Read response content;
    :Extract headers;
    if (Headers indicate error?) then (yes)
        :Build error details;
        stop
    else (no)
        :Return "Ok";
        stop
    endif
else (no)
    :Return exception message;
    stop
endif
```

# 使い方
## 0. cloneして移動
```sh
git clone https://github.com/kwhrkzk/plantuml-validator-mcp-server.git
cd plantuml-validator-mcp-server
```

## 1. Docker Compose実行
以下のコマンドを実行してサーバーを起動します。

```bash
docker compose up -d
```

## 2. VSCodeのMCP設定
[Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_configuration-format)が参考になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/7439a7c1-1325-4740-b0a7-645464ecfd9a.png)

### MCP:List Servers コマンド
VSCodeのMCP (Model Context Protocol) 拡張機能で利用可能なコマンドの一つです。このコマンドは、現在設定されているMCPサーバーのリストを表示します。

主な用途
MCPサーバーの確認: 現在VSCodeに設定されているMCPサーバーの一覧を確認できます。
サーバーの状態確認: 各サーバーの接続状態や設定内容を確認するのに役立ちます。

### MCP: Add Server コマンド
VSCodeのMCP (Model Context Protocol) 拡張機能で利用可能なコマンドの一つで、新しいMCPサーバーを設定に追加するために使用されます。

主な用途
新しいサーバーの登録: MCPサーバーをVSCodeの設定に追加します。
サーバーの接続設定: サーバーのURLやタイプ（例: SSE）を指定して、MCPサーバーとの通信を可能にします。

```json: settings.json
    "mcp": {
        "servers": {
            "my-plantuml-mcp-server": {
                "type": "sse",
                "url": "http://localhost:3000/sse"
            }
        }
    }
```

エージェントモードでスパナみたいなマークを押してサーバーと接続するとツールが見えていたら準備完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/90c90d63-fd22-4f66-b026-5bbe20088469.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/477de138-4987-4b28-b1ab-a37534ed1e12.png)


# 実行例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/8fc6b5ed-0b54-46d3-96ce-caf945b7304e.png)

その後何回かやりとりしましたけど出来上がったのが上のシーケンス図になります。

# 所感
Model Context Protocol (MCP)すごいですね。
XXXと連携できたらなぁという課題を一気に解決してくれる気がします。
実装も簡単で、実現したいところだけを書くだけでした。
コードもほとんどgithub Copilotがやってくれました。

[PostgreSQLのMCPサーバー](https://github.com/modelcontextprotocol/servers/tree/main/src/postgres)もあるようなのでよくわからないDBのテーブル構造の解析なんかにも使える気がしてきました。