ChatGPT から社内やローカルネットワーク上の Redmine を操作するため、OpenAI Tunnel Client と Redmine MCP Server(@onozaty/redmine-mcp-server)を CentOS Stream 9 上で常時稼働させる環境を構築しました。
ローカル PC(macOS など)で Tunnel Client を立ち上げる構成と比べ、Linux サーバーへ移して systemd でデーモン化することで、常時接続の安定性と運用のしやすさが大幅に向上します。
本記事では、最終的な構成ファイル(systemd unit / 環境変数)とセットアップ手順に加え、実際に構築する中で直面したトラブル(Node.js バージョン、systemd の仕様、npm キャッシュ権限など)の切り分けと解決策を整理して紹介します。
全体アーキテクチャ
目指す通信フローと責務の分離は次の通りです。
ChatGPT
|
v
OpenAI Control Plane
|
v (Tunnel)
CentOS Stream 9
|
+-- systemd (プロセス監視・自動再起動)
|
+-- tunnel-client-runtime-cloudflared
|
+-- stdio (標準入出力で中継)
|
+-- npx -y @onozaty/redmine-mcp-server
|
v (REST API)
Redmine
最終的な構成(完成形)
先に、動作が確認できている最終的なディレクトリ構成と設定ファイルを掲載します。最短で環境を作りたい場合はこの構成を参照してください。
ディレクトリ構成
Linux の標準的なディレクトリ階層(FHS)に沿って、実行ファイル・設定・キャッシュ・サービス管理の責務を分離しています。
| パス | 役割 | 権限 / 所有者 |
|---|---|---|
/opt/tunnel-client/ |
Tunnel Client 実行バイナリの配置先 | tunnel-client:tunnel-client |
/etc/tunnel-client/tunnel-client.env |
API キーや接続先の設定ファイル |
root:tunnel-client (640) |
/var/cache/tunnel-client/npm/ |
npx 実行時に使用する npm キャッシュ |
tunnel-client:tunnel-client (750) |
/etc/systemd/system/tunnel-client.service |
systemd サービス定義ファイル |
root:root (644) |
設定ファイル
1. systemd unit 定義 (/etc/systemd/system/tunnel-client.service)
[Unit]
Description=OpenAI Secure MCP Tunnel Client - Redmine
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=tunnel-client
Group=tunnel-client
WorkingDirectory=/opt/tunnel-client
EnvironmentFile=/etc/tunnel-client/tunnel-client.env
ExecStart=/opt/tunnel-client/tunnel-client-runtime-cloudflared run \
--control-plane.api-key env:CONTROL_PLANE_API_KEY \
--control-plane.tunnel-id env:CONTROL_PLANE_TUNNEL_ID \
--mcp.command "channel=main,command=/bin/npx -y @onozaty/redmine-mcp-server"
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
2. 環境変数ファイル (/etc/tunnel-client/tunnel-client.env)
# OpenAI Control Plane 接続情報
CONTROL_PLANE_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxx
CONTROL_PLANE_TUNNEL_ID=tunnel_xxxxxxxxxxxxxxxxxxxx
# Redmine 接続情報
REDMINE_URL=https://redmine.example.com/redmine
REDMINE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
REDMINE_MCP_READ_ONLY=false
# npm / npx キャッシュディレクトリの指定
NPM_CONFIG_CACHE=/var/cache/tunnel-client/npm
セットアップ手順
1. 実行用ユーザーとディレクトリの準備
セキュリティのため、root 権限ではなく専用のシステムユーザーで稼働させます。あわせて、npm のキャッシュ書き込み先ディレクトリも作成しておきます。
# 専用システムユーザーの作成
useradd \
--system \
--home-dir /opt/tunnel-client \
--shell /sbin/nologin \
tunnel-client
# キャッシュディレクトリの作成と権限設定
mkdir -p /var/cache/tunnel-client/npm
chown -R tunnel-client:tunnel-client /var/cache/tunnel-client
chmod 750 /var/cache/tunnel-client
2. Node.js の準備(Node.js 18+ 必須)
Redmine MCP Server(および MCP SDK)は Node.js 18 以上 を要求します。OS デフォルトのリポジトリから古い Node.js(v16 など)が入っている場合は、NodeSource 等を利用して最新の LTS(v20 または v22 推奨)をインストールしてください。
# Node.js バージョンの確認
node -v
# -> v20.x.x や v22.x.x 等、18以上であることを確認
3. Tunnel Client バイナリの配置
/opt/tunnel-client に実行ファイルを配置し、動作確認を行います。
# 配置と権限付与
chmod +x /opt/tunnel-client/tunnel-client-runtime-cloudflared
chown -R tunnel-client:tunnel-client /opt/tunnel-client
# バージョン確認
/opt/tunnel-client/tunnel-client-runtime-cloudflared --version
4. 設定ファイルの作成と権限設定
環境変数ファイルを作成し、API キー等の秘密情報を保護するためパーミッションを絞ります。
mkdir -p /etc/tunnel-client
vi /etc/tunnel-client/tunnel-client.env
# (前述の環境変数を記述)
chown root:tunnel-client /etc/tunnel-client/tunnel-client.env
chmod 640 /etc/tunnel-client/tunnel-client.env
5. サービスの登録と起動
# unit ファイルを作成
vi /etc/systemd/system/tunnel-client.service
# 設定の反映と起動
systemctl daemon-reload
systemctl enable --now tunnel-client
# 状態確認
systemctl status tunnel-client
構築時のハマりどころと切り分けの勘所
一見シンプルに見える構成ですが、複数のレイヤー(OS・言語ランタイム・systemd・MCP・クラウド接続)が絡むため、順番にトラブルが発生しました。
1. Node.js / npm のバージョン不整合(EBADENGINE)
発生した事象
単体で MCP サーバーを起動しようとした際、大量の警告が出現しました。
npm WARN EBADENGINE Unsupported engine {
package: '@modelcontextprotocol/sdk@1.15.0',
required: { node: '>=18' },
current: { node: 'v16.20.2', npm: '8.19.4' }
}
ここで「npm の更新通知が出ているから」と先に npm install -g npm を実行すると、npm 自身が Node.js 16 に対応しておらず更新を拒否されます。
対処法
npm だけを単体で引き上げようとせず、Node.js 自体を 18 以上のバージョンへ更新します。Node.js を更新すれば、対応する npm も自動的に付随して更新されます。
2. systemd EnvironmentFile に export を書いてしまう
発生した事象
手動テスト用のシェルスクリプトと同じ感覚で、環境変数ファイルに export KEY=VALUE と記述したところ、次のログが出力されて変数が全滅しました。
Ignoring invalid environment assignment 'export REDMINE_URL=...'
Ignoring invalid environment assignment 'export CONTROL_PLANE_TUNNEL_ID=...'
その結果、Tunnel Client からは環境変数が一切見えなくなり、tunnel ID is required というエラーで落ちていました。
対処法
EnvironmentFile はシェルスクリプトではなく、Key-Value 形式の定義ファイルです。export は記述せず KEY=VALUE の形式で定義します。
# 既存ファイルから export を一括削除する場合
sed -i 's/^export[[:space:]]*//' /etc/tunnel-client/tunnel-client.env
3. --cloudflared.managed オプションによる認証エラー
発生した事象
起動コマンドに --cloudflared.managed を付与していたところ、以下のエラーで停止しました。
cloudflared: fetch managed runtime credentials failed
対処法
今回は Redmine MCP Server を stdio(ローカルプロセス) で Tunnel Client に接続する構成です。Cloudflare のマネージドランタイム認証は不要なため、起動引数から --cloudflared.managed を削除することで解決しました。
4. npx 実行時の npm キャッシュ権限エラー(EACCES)
発生した事象
Tunnel Client から npx コマンド経由で MCP サーバーをキックした直後、npm がパーミッションエラーで異常終了しました。
npm error code EACCES
npm error syscall mkdir
npm error path /opt/tunnel-client/.npm
npm error errno EACCES
MCP サーバー(子プロセス)が終了したため、親プロセスである Tunnel Client も stdio MCP command failed; requesting tunnel-client shutdown を検知して道連れで終了していました。
対処法
tunnel-client ユーザーのホームディレクトリ配下にキャッシュを作ろうとした際、書き込み権限がないことが原因です。
運用の観点からも、可変データであるキャッシュは /var/cache/ 配下に分離するのが適切です。
-
/var/cache/tunnel-client/npmを作成し、所有者をtunnel-clientに設定 - 環境変数ファイルに
NPM_CONFIG_CACHE=/var/cache/tunnel-client/npmを追記
5. systemd 再起動ループ時の調査テクニック
Restart=always かつ RestartSec=5 に設定していると、子プロセスのエラーによって高速で再起動ループに陥り、ログが流れて原因の切り分けが難しくなります。
原因調査を行う際は、一旦サービスを停止し、手動で状態をリセットしてから確認することをおすすめします。
# サービスを一旦停止して失敗カウントをリセット
systemctl stop tunnel-client
systemctl reset-failed tunnel-client
# 起動して直近のログを確認
systemctl start tunnel-client
journalctl -u tunnel-client -n 50 --no-pager
動作確認とヘルスチェック
正常にサービスが立ち上がると、以下のように段階を踏んで接続が完了します。
stdio MCP command started
health server listening
starting control-plane poller
poller started
tunnel metadata fetched name="Redmine MCP"
Tunnel Client 内部でヘルスチェック用エンドポイントが起動しているため、ローカルから疎通を確認できます。
# 生存確認 (Liveness)
curl -i http://127.0.0.1:8080/healthz
# 準備状態確認 (Readiness)
curl -i http://127.0.0.1:8080/readyz
どちらも HTTP/1.1 200 OK が返ってくれば、サーバー側の常駐化は完了です。ChatGPT のインターフェース側からツール呼び出しができるか確認してください。
切り分けのステップ(まとめ)
エラーが出た際は、全体を一括で疑うのではなく、下層から順に成否を切り分けると原因を迅速に特定できます。
-
Tunnel Client 単体:
--versionが通り、バイナリが実行できるか - Node.js ランタイム: バージョンが 18 以上になっているか
-
MCP Server 単体: 必要な環境変数(
REDMINE_URL等)を export した状態で手動起動できるか -
systemd の環境変数読み込み:
systemctl show tunnel-client --property=Environment等で変数が渡っているか -
Control Plane 疎通:
tunnel metadata fetchedまで到達しているか -
stdio 実行権限:
npxのキャッシュ書き込み権限(EACCES)で落ちていないか
このパターンを一度構築してしまえば、Redmine 以外の stdio 接続型 MCP Server(GitLab、PostgreSQL、Slack 等)を Linux 上で常駐運用する際にも、そのままテンプレートとして横展開できます。