2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go言語で開発しているTWSNMP FKのMCPサーバーのセキュリティーを強化する方法

Posted at

はじめに

Go言語で開発しているネットワーク管理ソフトTWSNMP FKにMCPサーバー機能を追加した時のノウハウを

で紹介しました。
今回は、その続きでMCPサーバーのセキュリティーを強化する方法を紹介します。

開発とテスト環境

以前はローカルの環境にOllama+LanCahinGoで開発しましたが、最近は、

image.png

の環境で開発とテストをしています。

テスト用の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サーバー

設定

設定は

image.png

の画面です。
トランスポートとエンドポイント(バインドする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
}

処理の流れとしては

  1. MCPサーバーを作成する
  2. ツールを登録する
  3. HTTPサーバーを作成する、証明書があればTLS対応のHTTPサーバーに設定する
  4. echoサーバーを作成する
  5. ehcoサーバーにハンドラーを登録する
  6. サーバーを起動する

サーバー証明書

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すべてのメソッドに対応することがポイントです。ハンドラーの処理は、

  1. アクセス制限をチェックする
  2. 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サーバーの追加を実施します。

image.png

のようなダイアログから

http://:ポート/mcp

で登録できます。
名前、識別子は、お好きな名前でよいです。

登録すれば、MCPサーバーのリストに表示されます。
AIエージェントなどのツールに登録できます。

image.png

MCPサーバーのツールを利用して

image.png

のようなチャットができます。

n8nから使う

n8nの場合は、AI AgentのToolにMCPクライアントを追加します。

image.png

設定を開いて

image.png

TWSNMP FKのMCPサーバーのURLと必要に応じて認証設定を行います。

n8nのAIエージェントがMCPサーバーのツールを使ってくれます。

image.png

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?