Skip to content
Merged
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
66 changes: 62 additions & 4 deletions nixos/host/seminar/mcp-nixos.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
`https://mcp-nixos.ncaq.net/mcp`
読み取り専用のMCPサーバなので認証情報は不要です。
万が一の危険を減らすために仮想マシンで隔離しています。

mcp-nixos(fastmcp)自体はfaviconを提供しないため、
Caddyを前段に置いてfaviconとトップページを直接配信しています。
以前はCloudflareのリダイレクトルールでGitHub上のfaviconに302で誘導していましたが、
MCPクライアントのfaviconリゾルバ(Google s2など)はクロスホストのリダイレクトを追跡せず、
サブドメインにfaviconが無いと判定して親ドメインncaq.netのfaviconに、
フォールバックしてしまうため、200で直接返す方式に変更しました。
*/
{
pkgs,
Expand All @@ -13,6 +20,37 @@
}:
let
addr = config.machineAddresses.mcp-nixos;
# mcp-nixosリポジトリのwebサイトで使われているfaviconです。
# コミットをpinして取得の再現性を保証します。
favicon = pkgs.fetchurl {
url = "https://raw.githubusercontent.com/utensils/mcp-nixos/30530114f777483d2443ebc8347c3330387e66ea/website/public/favicon/favicon.ico";
Comment thread
ncaq marked this conversation as resolved.
hash = "sha256-uapx8X64FusNDFjj1NN0nLm9cbXMP04TVvaJVpoWJuk=";
};
indexHtml = pkgs.writeText "mcp-nixos-index.html" ''
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mcp-nixos.ncaq.net</title>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<h1>mcp-nixos.ncaq.net</h1>
<p>
This host serves
<a href="https://github.com/utensils/mcp-nixos">mcp-nixos</a>
as a remote MCP server.
</p>
<p>MCP endpoint: <code>https://mcp-nixos.ncaq.net/mcp</code></p>
</body>
</html>
'';
webRoot = pkgs.runCommand "mcp-nixos-webroot" { } ''
mkdir -p $out
cp ${favicon} $out/favicon.ico
cp ${indexHtml} $out/index.html
'';
in
{
# 仮に脆弱性があった場合の被害を最小限に抑えるため仮想マシンで動かします。
Expand All @@ -24,7 +62,7 @@ in
hypervisor = "cloud-hypervisor";
vsock.cid = config.microvmCid.mcp-nixos;
vcpu = 1;
# 実測ピークが約494MiB(NixOS基盤 + Python + mcp-nixos込み)なので、
# ピークが約500MiB(NixOS基盤 + Python + mcp-nixos + caddy込み)なので、
Comment thread
ncaq marked this conversation as resolved.
# 約30%の余裕を持って640MiBにしています。
mem = 640;
interfaces = [
Expand All @@ -49,6 +87,25 @@ in
hostName = "mcp-nixos";
firewall.allowedTCPPorts = [ 8080 ]; # Cloudflare Tunnelから転送されるポートです。
};
# faviconとトップページを直接配信し、
# MCPエンドポイントはmcp-nixosへ転送します。
# TLSはCloudflareが終端するのでHTTPのみです。
# ホスト名なしのポート指定にすることでCaddyの自動HTTPSを無効にしています。
# なおCaddyのreverse_proxyはtext/event-streamを検出すると、
# 自動でバッファリングを無効にするため、
# MCPのStreamable HTTP(SSE)向けの追加設定は不要です。
services.caddy = {
Comment thread
ncaq marked this conversation as resolved.
enable = true;
virtualHosts.":8080".extraConfig = ''
handle /mcp* {
Comment thread
ncaq marked this conversation as resolved.
reverse_proxy http://127.0.0.1:8081
Comment thread
ncaq marked this conversation as resolved.
}
handle {
root * ${webRoot}
file_server
}
'';
};
systemd = {
network = {
enable = true;
Expand All @@ -73,11 +130,12 @@ in
serviceConfig = {
# mcp-nixosはHTTPトランスポートをネイティブにサポートしているので、
# 環境変数で設定して直接起動します。
# パスは未指定だとデフォルトの`/mcp`になり、Cloudflare Tunnelの転送先と一致します。
# パスは未指定だとデフォルトの`/mcp`になり、前段Caddyの転送先と一致します。
# 外部からはCaddy経由でアクセスするのでループバックのみで待ち受けます。
Environment = [
"MCP_NIXOS_TRANSPORT=http"
"MCP_NIXOS_HOST=0.0.0.0"
"MCP_NIXOS_PORT=8080"
"MCP_NIXOS_HOST=127.0.0.1"
Comment thread
ncaq marked this conversation as resolved.
"MCP_NIXOS_PORT=8081"
];
ExecStart = "${pkgs.mcp-nixos}/bin/mcp-nixos";
DynamicUser = true;
Expand Down
Loading