はじめに
Go言語で開発しているネットワーク管理ソフトTWSNMP FKにMCPサーバー機能を追加した時のノウハウを
で紹介しました。
今回は、その続きでMCPサーバーのセキュリティーを強化する方法を紹介します。
開発とテスト環境
以前はローカルの環境にOllama+LanCahinGoで開発しましたが、最近は、
の環境で開発とテストをしています。
テスト用のLLMはMac mini M2 ProにOllama
を直接インストールしてモデルはQwen3を使いました。
latest=8bでも、それなりに動作します。
ここまでは以前と同じです。
テストするためのAIエージェントはDify
または、n8n
を使いました。Docker環境にローカルインストールしています。
どちらもGUIから操作できるので簡単にMCPサーバーを試せます。
n8nには認証設定もあるので認証付きのMCPサーバーも試せます。
利用したGo言語のパッケージ
MCPサーバー
MCPサーバーは、活発に開発が続いているので前回と同じ
を使用しました。
HTTPサーバー
MCPサーバーのパッケージにHTTPサーバー機能は組み込まれていますが、HTTPS(TLS)対応やアクセス元のIPアドレスを制限するためには別のHTTPサーバーパッケージを使う必要がありました。
TWSNMP FCでも使っているEcho
を使いました。
実装
MCPサーバー
設定
設定は
の画面です。
トランスポートとエンドポイント(バインドするIPとポート)は以前と同じです。
トランスポートにOFFを選択できるようにして停止することもできます。
今回は、セキュリティーを高めるために、アクセス元のIP制限とMCPサーバーにアクセスするためのトークンを追加しました。トークンを自動生成するボタンとコピーするボタンもつけました。
MCPサーバーの起動
MCPサーバーを起動する処理は
func startMCPServer() (any, *echo.Echo) {
// Create MCP Server
s := server.NewMCPServer(
"TWSNMP FK MCP Server",
"1.25.0",
server.WithToolCapabilities(true),
server.WithLogging(),
)
// Add tools to MCP server
addGetNodeListTool(s)
addGetNetworkListTool(s)
addGetPollingListTool(s)
sv := &http.Server{}
sv.Addr = datastore.MapConf.MCPEndpoint
// check TLS
if cert, err := getMCPServerCert(); err == nil {
if cert != nil {
sv.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{*cert},
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
},
MinVersion: tls.VersionTLS13,
}
}
} else {
log.Printf("getMCPServerCert err=%v", err)
}
// Create echo server
e := echo.New()
e.HideBanner = true
e.HidePort = true
var mcpsv any = nil
// Add Handler
if datastore.MapConf.MCPTransport == "sse" {
sseServer := server.NewSSEServer(s)
e.Any("/sse", func(c echo.Context) error {
if !checkMCPACL(c) {
return echo.ErrUnauthorized
}
sseServer.ServeHTTP(c.Response().Writer, c.Request())
return nil
})
e.Any("/message", func(c echo.Context) error {
if !checkMCPACL(c) {
return echo.ErrUnauthorized
}
sseServer.ServeHTTP(c.Response().Writer, c.Request())
return nil
})
log.Printf("sse mcp server listening on %s", datastore.MapConf.MCPEndpoint)
mcpsv = sseServer
} else {
streamServer := server.NewStreamableHTTPServer(s)
e.Any("/mcp", func(c echo.Context) error {
if !checkMCPACL(c) {
return echo.ErrUnauthorized
}
streamServer.ServeHTTP(c.Response().Writer, c.Request())
return nil
})
log.Printf("streamable HTTP server listening on %s", datastore.MapConf.MCPEndpoint)
mcpsv = streamServer
}
// Start server
go func() {
if err := e.StartServer(sv); err != nil {
log.Printf("start mcp server err=%v", err)
}
}()
return mcpsv, e
}
処理の流れとしては
- MCPサーバーを作成する
- ツールを登録する
- HTTPサーバーを作成する、証明書があればTLS対応のHTTPサーバーに設定する
- echoサーバーを作成する
- ehcoサーバーにハンドラーを登録する
- サーバーを起動する
サーバー証明書
MCPサーバーをHTTPS(TLS)対応で起動するためのサーバー証明書と秘密鍵は、以下の関数で読み込みます。
func getMCPServerCert() (*tls.Certificate, error) {
if datastore.MCPCert == "" || datastore.MCPKey == "" {
return nil, nil
}
keyPem, err := os.ReadFile(datastore.MCPKey)
if err == nil {
certPem, err := os.ReadFile(datastore.MCPCert)
if err == nil {
cert, err := tls.X509KeyPair(certPem, keyPem)
if err == nil {
return &cert, nil
}
}
}
return nil, err
}
ハンドラー
ehcoサーバーにハンドラーは、Anyを使ってGET,POST,HEADすべてのメソッドに対応することがポイントです。ハンドラーの処理は、
- アクセス制限をチェックする
- MCPのトランスポートサーバーへ処理を渡す
になっています。
アクセス制限のチェック
func checkMCPACL(c echo.Context) bool {
if datastore.MapConf.MCPToken != "" {
t := c.Request().Header.Get("Authorization")
log.Printf("checkMCPACL token=%+v", t)
if !strings.Contains(t, datastore.MapConf.MCPToken) {
return false
}
}
if datastore.MapConf.MCPFrom == "" {
return true
}
if ip, _, err := net.SplitHostPort(c.Request().RemoteAddr); err == nil {
if _, ok := mcpAllow.Load(ip); ok {
return true
}
}
if _, ok := mcpAllow.Load(c.RealIP()); ok {
return true
}
return false
}
トークンのチェックとアクセス元IPのチェックを行っています。
もう少しセキュリティーを強化したい場合は、トークンにJWTなどを使ってトークンに期限を設けて運用するのがよいと思います。
TWSNMP FCでは、
mcpStreamableHTTPServer = server.NewStreamableHTTPServer(s)
e.Any("/mcp", func(c echo.Context) error {
if !mcpCheckFromAddress(c) {
return echo.ErrUnauthorized
}
mcpStreamableHTTPServer.ServeHTTP(c.Response().Writer, c.Request())
return nil
}, echojwt.JWT([]byte(p.Password)))
のように、echoのJWT対応ミドルうウェア
を使っています。
アクセス元IPアドレスの制限
アクセス元のIPアドレス制限は、許可するIPアドレスをカンマ区切りの文字列から
func setMCPAllow() {
for _, ip := range strings.Split(datastore.MapConf.MCPFrom, ",") {
ip = strings.TrimSpace(ip)
if ip != "" {
mcpAllow.Store(ip, true)
}
}
}
の関数で読み込んでいます。
サーバー停止
stopMCPServer := func() {
if mcpsv == nil {
return
}
log.Println("stop mcp server")
datastore.AddEventLog(&datastore.EventLogEnt{
Type: "system",
Level: "info",
Event: i18n.Trans("Stop MCP server"),
})
switch m := mcpsv.(type) {
case *server.SSEServer:
m.Shutdown(ctx)
case *server.StreamableHTTPServer:
m.Shutdown(ctx)
}
mcpsv = nil
if e != nil {
e.Shutdown(ctx)
e = nil
}
}
のようにMCPサーバーのトランスポートを終了した後echoサーバーを終了します。
ツールの登録
PINGを実行するツールの登録処理は、
type mcpPingEnt struct {
Result string `json:"Result"`
Time string `json:"Time"`
RTT string `json:"RTT"`
RTTNano int64 `json:"RTTNano"`
Size int `json:"Size"`
TTL int `json:"TTL"`
ResponceFrom string `json:"ResponceFrom"`
Location string `json:"Location"`
}
func addDoPingtTool(s *server.MCPServer) {
searchTool := mcp.NewTool("do_ping",
mcp.WithDescription("do ping"),
mcp.WithString("target",
mcp.Required(),
mcp.Description("ping target ip address or host name"),
),
mcp.WithNumber("size",
mcp.DefaultNumber(64),
mcp.Max(1500),
mcp.Min(64),
mcp.Description("ping packate size"),
),
mcp.WithNumber("ttl",
mcp.DefaultNumber(254),
mcp.Max(254),
mcp.Min(1),
mcp.Description("ip packet TTL"),
),
mcp.WithNumber("timeout",
mcp.DefaultNumber(2),
mcp.Max(10),
mcp.Min(1),
mcp.Description("timeout sec of ping"),
),
)
s.AddTool(searchTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
target, err := request.RequireString("target")
if err != nil {
return mcp.NewToolResultText(err.Error()), nil
}
target = getTragetIP(target)
if target == "" {
return mcp.NewToolResultText("target ip not found"), nil
}
timeout := request.GetInt("timeout", 3)
size := request.GetInt("size", 64)
ttl := request.GetInt("ttl", 254)
pe := ping.DoPing(target, timeout, 0, size, ttl)
res := mcpPingEnt{
Result: pe.Stat.String(),
Time: time.Now().Format(time.RFC3339),
RTT: time.Duration(pe.Time).String(),
Size: pe.Size,
ResponceFrom: pe.RecvSrc,
TTL: pe.RecvTTL,
RTTNano: pe.Time,
}
if pe.RecvSrc != "" {
res.Location = datastore.GetLoc(pe.RecvSrc)
}
j, err := json.Marshal(&res)
if err != nil {
j = []byte(err.Error())
}
return mcp.NewToolResultText(string(j)), nil
})
}
処理の流れとしては
- NewToolでツールの仕様を作成(引数と説明)
- AddToolで登録
です。AddToolにツールが処理する関数を渡します。
処理関数の流れは
- requestからパラメータを取り出す
- 処理をする
- 結果をJSONにする
- 結果を返す
です。必須のパラメータがないとエラーを返します。
結果は、テキストでもよいのですがJSONにしたほうが正確いAI側に伝わると思います。
他のツールも同じように登録できます。
これは、前回紹介した記事と同じです。
Difyから使う
ツールのMCPタブを選択してMCPサーバーの追加を実施します。
のようなダイアログから
http://:ポート/mcp
で登録できます。
名前、識別子は、お好きな名前でよいです。
登録すれば、MCPサーバーのリストに表示されます。
AIエージェントなどのツールに登録できます。
MCPサーバーのツールを利用して
のようなチャットができます。
n8nから使う
n8nの場合は、AI AgentのToolにMCPクライアントを追加します。
設定を開いて
TWSNMP FKのMCPサーバーのURLと必要に応じて認証設定を行います。
n8nのAIエージェントがMCPサーバーのツールを使ってくれます。