KEELããŒã ã®çžåã§ãã ååã®ãšã³ããªã¯ãå°ããçµè·¯æé©åããã«ãŠã§ã¢ãå®è£
ããŠããããAZééä¿¡ãåæžãããã§ããã www.lifull.blog ä»åã¯ãMCPãµãŒããæ¯èŒçå®å
šã«åããããã«è²ã
ãã£ãŠãè©±ãæžããããšæããŸãã MCPã«ã€ã㊠MCPãµãŒãã®ãªã¹ã¯ ãªãã¹ãããŒã«ã«ã§åãããªã ããŒã«ã«ã§ã¯ãããŠDockerã§åãã ç¡çããHTTP Transportã«å¯Ÿå¿ãã ã»ãã·ã§ã³ã®éå§ ã³ãã³ãã®èµ·å ãã€ããŒããåãåãããã®ãšã³ããã€ã³ãã®å®è£
ãŸãšã MCPã«ã€ã㊠MCPã¯Model Context Protocolã®ç¥ã§ãAnthropicãæšæºåãäž»å°ããLLMãšãã®å€éšãç¹ããããã³ã«ã§ãã github.com ããã«ããGitHubãPlaywrightãšãã£ãå€éšã®ããŒã«ãLLMãèªåŸçã«å©çšã§ããããã«ãªããOpenAIããµããŒãã衚æããããšãã倧ããªæ³šç®ãéããŠããŸãã èŠã¯æšæºåãããFunction Callingãšããããšã«ãªãã§ãããã æã
KEELããŒã ã§ã¯2幎ã»ã©åããç¡éã«ã¹ã±ãŒã«ããæ±çšAI(ä»®)ãéçºããŠããŠãããããã¯å
éšã§Function CallingãããšãŒãžã§ã³ããè€æ°å調ãããŠåããããšã§ããããã¿ã¹ã¯ã解決ããããšãçã£ãŠããŸãã www.lifull.blog Function Calling(MCPãµãŒã)ãæšªã«å€§éã«äžŠã¹ãŠäœ¿ãããšãããšããŒã¯ã³æ¶è²»ãç¡èŠã§ããªããªãã¯ãã§ãåœææã
ã¯ããã解決ããããã«ãã«ããšãŒãžã§ã³ããšããŠè€æ°ã®ãšãŒãžã§ã³ããå調ãããããšãéžã³ãŸããã (æè¿ã® A2A ã®åããèŠãŠããŠãããŸã ãã®èšèšã§ãã£ãŠããããã ãªãšæããŠããŸã) ãŸãããšãŒãžã§ã³ããç°¡åã«éçºã§ãããã¬ãŒã ã¯ãŒã¯ãæäŸããããšã§ã瀟å
ã®Contributorãèªç±ã«ç€Ÿå
ã·ã¹ãã ãšã®é£æºãå®è£
ã§ããããã«ããªã£ãŠããŸãã ãšããããšããããæã
ç®ç·ã§ã¯MCPã®ç»å Žã¯ãã³ãã¥ããã£ãæçããã°Function Callingå®è£
ã®æéãçãããªïœããããã®åãæ¢ãæ¹ã ã£ãããã§ãããä»ãšãªã£ãŠã¯è·çš®åããå€ãã®äººãããŒã«ã«ã§MCPãµãŒããåããããšãã瀟å
ã·ã¹ãã ã®MCPãµãŒããéçºãããããã«ãªã£ãŠããŸããã ããããMCPãç¡èŠã§ããªããªã£ãŠããŠããã(æ¬æ¥ã¯ãã«ãããã³ããªKubernetesã¯ã©ã¹ã¿ã®éçºã§ã¯ããã€ã€)ãã©ãããã©ãŒããŒãšããŠåãããšã«ããŸããã MCPãµãŒãã®ãªã¹ã¯ 2025幎5æçŸåšã®ããããMCPãµãŒãã®å®è¡æ¹æ³ã¯ npx ã docker run çµç±ã®ãã®ã ãšçè§£ããŠããŸãã Docker for DesktopæååãšãCPU Virtualizationãå¿
èŠã§ããããšãèãããšãè·çš®åããå©çšè
ãå€ãã®ã¯ npx ã§ããããã å€ãã®å Žåã©ã¡ããçŽ æŽã«ææ°ã®MCPãµãŒãã®å®è£
ãååŸããŠããŠãå®è¡ãŠãŒã¶ã®æš©éã§stdio Transportçµç±ã§MCPãã¹ããå©çšããŸãã ãããŠãã®å Žåã æªæã®ããMCPãµãŒãã誀ã£ãŠå®è¡ããŠããŸããšãèžã¿å°ãšããŠå©çšãããã npx çµç±ã§ã®å®è¡ã§ã¯ç°å¢å€æ°ããã¡ã€ã«ã奪ãããŠããŸããšãããªã¹ã¯ããããŸãã æè¿ã§ã¯MCPãã¹ãã®UIããã¯èŠããªã圢ã§LLMã«å¯ŸããŠæªæã®ããæç€ºãããæ»æææ³ãçºèŠããããªã©ãMCPãµãŒãã察象ã«ããæ»æã¯å·§åŠåãã€ã€ããæ³šæãå¿
èŠã§ãã(ããã§ãç§ãã¡ã¯Auto-approveã䜿ã£ãŠããŸãããã§ãã) invariantlabs.ai ãšããããããŒãžã§ã³ã¯åºå®ãããã§ãããããŒãžã§ã³ãããã°ã©ã ã®äžèº«ãäžæã«èå¥ãããã®ã§ãªãããšãèãããšãæäœé docker run ã§ã¯Digestã§ã®åç
§ããä»ãChecksumãŸã§ããããšçæ³ã§ãããã ãã ããã ãã§ã¯ãŸã äžååã ãšæããŠããŠã欲ãèšãã°WebAssemblyãDenoã®ãããªãµã³ãããã¯ã¹äžã§æç€ºçã«Capabilityãäžããäžã§å®è¡ããããšããã§ãã äžæ¹ã§ãããã¯MCPãµãŒãã®å®è£
ã«äŸåããéšåãå€ãããããã®äžã§æ¯èŒçå®å
šã«MCPãµãŒããéçšã§ããªãããšè²ã
ãã£ãŠããŸããã ãªãã¹ãããŒã«ã«ã§åãããªã ããããåã
ãããŒã«ã«ã§å€§éã®MCPãµãŒããå®è¡ããããšãäŒç€Ÿçã«ã¯å°ãå³ããããããšæããŠããŸãã ãã¡ããPlaywrightã®MCPãµãŒãã®ããã«æå
ã®ãã¹ã¯ãããç°å¢ãæäœãããå Žåã¯ããŒã«ã«ã§åããå¿
èŠããããŸããã瀟å
ã·ã¹ãã çšã®MCPãµãŒããOrganizationã¬ãã«ã§æš©éãæã£ãGitHubãªã©ã®MCPãµãŒãã¯ã瀟å
å
±éã§äžã€åãããŠãããŠãããåã
ãåç
§ããã ãã§ããå¯èœæ§ããããŸãã MCPã«ã¯ããã€ãã®Transportå®è£
ãããããã䜿ããã(?)stdio Transportã¯MCPãã¹ããçŽæ¥ã³ãã³ããšããŠMCPãµãŒããå®è¡ãããã®åã®éãstdioãä»ããŠMCPãµãŒããšããåããããŸãã äžæ¹ã§ãServer Sent Eventsãå©çšããTransportå®è£
ããããæ¹è¯ãã Streamable HTTP Transport ãšåŒã°ããHTTPã«èŒã£ããã®ãããããããã¯ãªã¢ãŒãã«ããMCPãµãŒããå©çšå¯èœã§ãã ãã®ãããLIFULLã§ã¯ãããã®Transportãå®è£
ããMCPãµãŒãã¯ãŸãšããŠåè¿°ã®ãã«ãããã³ããªKubernetesã¯ã©ã¹ã¿ã§éçšããŠæäŸããããã«ããŠããŸãã apiVersion : apps/v1 kind : Deployment spec : template : spec : automountServiceAccountToken : false securityContext : fsGroup : 65532 seccompProfile : type : RuntimeDefault containers : - name : mcp securityContext : privileged : false allowPrivilegeEscalation : false capabilities : drop : - ALL readOnlyRootFilesystem : true runAsUser : 65532 runAsNonRoot : true seccompProfile : type : RuntimeDefault <snip> Kubernetesã®ãã¹ããã©ã¯ãã£ã¹(Pod Security Standards)ã«æ²¿ã£ãŠå³æ Œãªæš©éã§åãããŠããŸãã æç€ºçã«èš±å¯ããªãéããMCPãµãŒãã¯rootfsãžã®æžãèŸŒã¿æš©éããäžããããŸããã ãŸããLIFULLã§ã¯ãã¹ãŠã®ã³ã³ããã«Istioã®ãããã·ãå
¥ããããšã矩åä»ããŠãããMCPãµãŒãã«ãåæ§ã«Istioã®ãããã·ãå
¥ããŠãèš±å¯ããæ¥ç¶å
以å€ã«ã¯ãªã¯ãšã¹ãã§ããªãããã«ããŠããŸãã apiVersion : networking.istio.io/v1beta1 kind : Sidecar spec : egress : - captureMode : DEFAULT hosts : - istio-system/istiod.istio-system.svc.cluster.local - istio-system/otel-agent.istio-system.svc.cluster.local outboundTrafficPolicy : mode : REGISTRY_ONLY <snip> www.lifull.blog ããã«ãããããŒã«ã«ã§åããå¿
èŠããªãMCPãµãŒãããã©ãããã©ãŒã ããæäŸããããšã§ããªãœãŒã¹ã®å¹çåãšãšãã«æ¯èŒçå®å
šã«éçšããããšãã§ããŸããã ããŒã«ã«ã§ã¯ãããŠDockerã§åãã ãšã¯ããå
šãŠã®MCPãµãŒãããªã¢ãŒãã§åãããããã§ã¯ãããŸããã åè¿°ã®ããã«ãã¹ã¯ãããç°å¢ã«äŸåãããããªMCPãµãŒãã¯ããŒã«ã«ã§åããå¿
èŠããããŸãããPersonal Access Tokenãå©çšããããåŸãªããã®ãåæ§ã§ãã ãã®éã¯ãKubernetesã§å®çŸããŠãããã®ãšäŒŒããããªéé¢ç©ºéãDocker Composeã䜿ã£ãŠå®çŸããããšãæšå¥šããŠããŸãã # docker-compose.yaml services : tcp-proxy : image : ghcr.io/lifull/keel/proxy:latest command : - https://example.com - --mode - tcp - --address - 0.0.0.0:443 networks : default : {} internal : ipv4_address : 172.16.255.1 my-mcp-server : image : ghcr.io/lifull/keel/my-mcp-server:latest cap_drop : - ALL environment : - PERSONAL_ACCESS_TOKEN=${PERSONAL_ACCESS_TOKEN} extra_hosts : - "example.com:172.16.255.1" networks : internal : {} networks : internal : internal : true ipam : driver : default config : - subnet : 172.16.0.0/16 default : driver : bridge Dockerã¯ããã©ã«ãã§bridgeãããã¯ãŒã¯äžã§ã³ã³ãããåãããbridgeãä»ããŠãã¹ãã®ãããã¯ãŒã¯ããã€ã³ã¿ãŒãããã«åºãŠãããŸãã ãã®ãããæªæã®ããMCPãµãŒããDockerã³ã³ãããšããŠåãããŠããŸããšãã®ãŸãŸå€ã«ãªã¯ãšã¹ããããŠããŸãããã§ãã ããã§ããã®äŸã§ã¯Docker Composeå
ã«éãããããã¯ãŒã¯ internal ãäœæããMCPãµãŒãã«ã¯ãã® internal ã®ã¿ãå²ãåœãŠãããšã§ã€ã³ã¿ãŒãããã«çéã§ããªãããã«ããŠããŸãã ãããããã®ã«ãã£ãŠã¯GitHubã®APIãå©ããªã©ã€ã³ã¿ãŒãããã«çéããå¿
èŠããããã®ããããŸãã ãã®éã¯TCP Proxyãåãã internal ãããã¯ãŒã¯å
ã«çšæããããã§ããã®TCP Proxyã®ã¿ãbridgeãããã¯ãŒã¯ã«ãå²ãåœãŠãããšã§ãèš±å¯ããæ¥ç¶å
ã«ã®ã¿ãªã¯ãšã¹ãã§ããããã«ããŠããŸãã extra_hosts ãå©çšããããšã§MCPãµãŒãã®ããã°ã©ã ãäžå倿Žããããšãªããå®å
šã«TCP Proxyãçµç±ãããããšãå¯èœã§ãã ãã¡ãã¡è§£èª¬ãããŸã§ããªããšæããŸãããTCP Proxyã®å®è£
ã¯ãããªæãã§ãã TCP Proxy func runTCPServer(listener net.Listener, target *url.URL, a *Args) error { shutdown := make ( chan struct {}, 1 ) semaphore := make ( chan struct {}, a.MaxConnections) wg := sync.WaitGroup{} go func () { for { local, err := listener.(*net.TCPListener).AcceptTCP() if err != nil { select { case <-shutdown: return default : continue } } semaphore <- struct {}{} wg.Add( 1 ) go func () { defer func () { <-semaphore wg.Done() }() defer local.Close() remoteAddress := target.Host if target.Port() == "" { switch target.Scheme { case "http" : remoteAddress = net.JoinHostPort(target.Hostname(), "80" ) case "https" : remoteAddress = net.JoinHostPort(target.Hostname(), "443" ) } } remote, err := net.DialTimeout( "tcp" , remoteAddress, 10 *time.Second) if err != nil { return } defer remote.Close() c := make ( chan struct {}, 2 ) f := func (c chan struct {}, dst io.Writer , src io.Reader ) { _, _ = io.Copy(dst, src) c <- struct {}{} } go f(c, remote, local) go f(c, local, remote) select { case <-c: case <-shutdown: local.CloseWrite() } }() } }() quit := make ( chan os.Signal, 1 ) signal.Notify(quit, syscall.SIGTERM) <-quit time.Sleep(time.Duration(a.Lameduck) * time.Second) close (shutdown) listener.Close() wg.Wait() return nil } docker-compose.yaml ãé
åžããã ãã§äŸ¿å©ãªMCPãµãŒãã瀟å¡ã«è¡ãå±ãããããšãã§ããã®ã§äžç³äºé³¥ã§ããã äžã€ãã¬ãŒããªããšããŠããã®å ŽåMCPãµãŒã㯠docker compose up ã§ãŸãšããŠèµ·åãããããå¿
ç¶çã«MCPã¯ã©ã€ã¢ã³ãããã¯stdio Transportã§ã¯ãªãHTTP Transportãå©çšããããšã«ãªããŸãã(JetBrainsã®Junieã¯HTTP Transportã«æªå¯Ÿå¿ã ã£ããããŸã) ç¡çããHTTP Transportã«å¯Ÿå¿ãã äžæ¹ã§ãäžã€ç®ã®ãªã¢ãŒãã§åããã«ããäºã€ç®ã®Docker Composeã§åããã«ãããMCPãµãŒããHTTP Transportã«å¯Ÿå¿ããŠããããšãåæãšãªããŸãã ãããéè¯ã®MCPãµãŒãã®äžã«ã¯stdio Transportããå®è£
ããŠããªããã®ããããªãã«ãããŸãã ãããã£ãMCPãµãŒãã§ã¯äžèšã®ã¢ãããŒããåããªãã®ã§å·¥å€«ãå¿
èŠã§ãã ããã§ãæã
KEELããŒã ã¯stdio Transportã«ã®ã¿å¯Ÿå¿ããŠããMCPãµãŒããç¡çããSSE Transportã«å¯Ÿå¿ããå°ãããããã·ãéçºããŸããã ãã€ããªãšããŠç°¡åã«é
åžã§ããããã«Goã§æžããŠããŸãã mcp-stdio-proxy package main import ( "bufio" "context" "encoding/json" "errors" "flag" "fmt" "log" "net" "net/http" "os" "os/exec" "os/signal" "runtime/debug" "sync" "syscall" "time" "github.com/google/uuid" ) type MCPMethod string // https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts const ( CancelledNotification MCPMethod = "notifications/cancelled" InitializeRequest = "initialize" InitializedNotification = "notifications/initialized" PingRequest = "ping" ProgressNotification = "notifications/progress" ListResourcesRequest = "resources/list" ListResourceTemplatesRequest = "resources/templates/list" ReadResourceRequest = "resources/read" ResourceListChangedNotification = "notifications/resource/list/changed" SubscribeRequest = "resources/subscribe" UnsubscribeRequest = "resources/unsubscribe" ResourceUpdatedNotification = "notifications/resource/updated" ListPromptsRequest = "prompts/list" GetPromptRequest = "prompts/get" PromptListChangedNotification = "notifications/prompt/list/changed" ListToolsRequest = "tools/list" CallToolRequest = "tools/call" ToolListChangedNotification = "notifications/tool/list/changed" ) type MCPMessage struct { JSONRPC string `json:"jsonrpc"` Method MCPMethod `json:"method"` ID any `json:"id,omitempty"` } type session struct { id string responseQueue chan [] byte requestQueue chan [] byte } func main() { var address string var terminationGracePeriodSeconds int var lameduck int var keepAlive bool var verbose bool flag.StringVar(&address, "address" , "0.0.0.0:8080" , "" ) flag.IntVar(&terminationGracePeriodSeconds, "termination-grace-period-seconds" , 10 , "The duration in seconds the application needs to terminate gracefully" ) flag.IntVar(&lameduck, "lameduck" , 1 , "A period that explicitly asks clients to stop sending requests, although the backend task is listening on that port and can provide the service" ) flag.BoolVar(&keepAlive, "http-keepalive" , true , "" ) flag.BoolVar(&verbose, "verbose" , false , "" ) flag.Parse() args := flag.Args() if len (args) == 0 { log.Fatalf( "command not specified" ) } name := args[ 0 ] var arg [] string if len (args) > 1 { arg = args[ 1 :] } sessions := &sync.Map{} mux := http.NewServeMux() mux.HandleFunc( "GET /sse" , func (w http.ResponseWriter, r *http.Request) { w.Header().Set( "Content-Type" , "text/event-stream" ) w.Header().Set( "Cache-Control" , "no-cache" ) w.Header().Set( "Connection" , "keep-alive" ) flusher, ok := w.(http.Flusher) if !ok { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } cmd := exec.Command(name, arg...) stdin, err := cmd.StdinPipe() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } stdout, err := cmd.StdoutPipe() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err := cmd.Start(); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } s := &session{ id: uuid.New().String(), requestQueue: make ( chan [] byte , 65536 ), responseQueue: make ( chan [] byte , 65536 ), } sessions.Store(s.id, s) go func () { scanner := bufio.NewScanner(stdout) for scanner.Scan() { s.responseQueue <- scanner.Bytes() } }() defer func () { sessions.Delete(s.id) _ = cmd.Process.Kill() _ = cmd.Wait() close (s.requestQueue) close (s.responseQueue) }() _, _ = fmt.Fprintf(w, "event: endpoint \n data: %s \n\n " , fmt.Sprintf( "http://%s/messages?sessionId=%s" , r.Host, s.id)) flusher.Flush() for { select { case data := <-s.requestQueue: _, _ = stdin.Write(data) case data := <-s.responseQueue: _, _ = fmt.Fprint(w, fmt.Sprintf( "event: message \n data: %s \n\n " , data)) flusher.Flush() case <-r.Context().Done(): return } } }) mux.HandleFunc( "POST /messages" , func (w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get( "sessionId" ) if id == "" { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } sany, ok := sessions.Load(id) if !ok { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } s := sany.(*session) var rawMessage json.RawMessage if err := json.NewDecoder(r.Body).Decode(&rawMessage); err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var mcpMessage MCPMessage if err := json.Unmarshal(rawMessage, &mcpMessage); err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if verbose { log.Printf( "%s: %s" , id, mcpMessage.Method) } s.requestQueue <- rawMessage s.requestQueue <- [] byte ( " \n " ) w.Header().Set( "Content-Type" , "application/json" ) w.WriteHeader(http.StatusAccepted) _, _ = w.Write([] byte (http.StatusText(http.StatusAccepted))) }) mux.HandleFunc( "GET /healthz" , func (w http.ResponseWriter, r *http.Request) { w.Header().Set( "Content-Type" , "text/plain; charset=utf-8" ) w.WriteHeader(http.StatusOK) _, _ = w.Write([] byte (http.StatusText(http.StatusOK))) }) listener, err := net.Listen( "tcp" , address) if err != nil { log.Fatalf( "failed to listen: %+v" , err) } server := &http.Server{ Handler: mux, } server.SetKeepAlivesEnabled(keepAlive) go func () { defer func () { if err := recover (); err != nil { log.Printf( "panic: %+v \n %s" , err, debug.Stack()) } }() if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf( "failed to listen: %+v" , err) } }() quit := make ( chan os.Signal, 1 ) signal.Notify(quit, syscall.SIGTERM) <-quit time.Sleep(time.Duration(lameduck) * time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(terminationGracePeriodSeconds)*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatalf( "failed to shutdown: %+v" , err) } } éçºåœæã¯Streamable HTTP Transportããªãã£ãã®ã§SSE Transportã«ã®ã¿å¯Ÿå¿ããŠããŸãã ããã§ã¯å®è£
ã軜ã解説ããŠãããŸãã ã»ãã·ã§ã³ã®éå§ SSE Transportã¯ãã¡ãã®ä»æ§ã®éãã /sse ã®ãããªSSEã®ãšã³ããã€ã³ãã§ååã®ãªã¯ãšã¹ããåãããšããã€ããŒããåãåãããã®å¥ã®ãšã³ããã€ã³ãã event: endpoint ãšããŠè¿ãå¿
èŠããããŸãã github.com mux.HandleFunc( "GET /sse" , func (w http.ResponseWriter, r *http.Request) { w.Header().Set( "Content-Type" , "text/event-stream" ) w.Header().Set( "Cache-Control" , "no-cache" ) w.Header().Set( "Connection" , "keep-alive" ) flusher, ok := w.(http.Flusher) if !ok { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } <snip> _, _ = fmt.Fprintf(w, "event: endpoint \n data: %s \n\n " , fmt.Sprintf( "http://%s/messages?sessionId=%s" , r.Host, s.id)) flusher.Flush() <snip> } ããã¯SSEããµãŒãããã¯ã©ã€ã¢ã³ãã«ã¡ãã»ãŒãžãéä¿¡ããããã®äžæ¹åã®ä»çµã¿ã§ããããã§ãã developer.mozilla.org ãã®ããSSE Transportã§ã¯2ã€ã®HTTPãšã³ããã€ã³ããå®è£
ããããšã§ãMCPã¯ã©ã€ã¢ã³ããšã®åæ¹åéä¿¡ãå®çŸããŠããŸãã ããäžã€ã®ãšã³ããã€ã³ãã®å®è£
ã¯åŸè¿°ãããããããã§ã¯SSEã®ãšã³ããã€ã³ãã®ã¿è§£èª¬ããŸãã sessions := &sync.Map{} mux.HandleFunc( "GET /sse" , func (w http.ResponseWriter, r *http.Request) { <snip> s := &session{ id: uuid.New().String(), requestQueue: make ( chan [] byte , 65536 ), responseQueue: make ( chan [] byte , 65536 ), } sessions.Store(s.id, s) <snip> defer func () { sessions.Delete(s.id) <snip> close (s.requestQueue) close (s.responseQueue) }() <snip> } UUIDãçºè¡ããŠã»ãã·ã§ã³ãéå§ããŠããŸãã ããã§éå§ãããã»ãã·ã§ã³ã¯MCPã¯ã©ã€ã¢ã³ãã«éç¥ãããã€ããŒããåãåãããã®ãšã³ããã€ã³ããããå©çšãããããäžã€äžã®ã¹ã³ãŒãã§ sessions ãšããŠä¿æããŸãã ã³ãã³ãã®èµ·å ã»ãã·ã§ã³ã®éå§ãšåæã«stdio Transportãå®è£
ããMCPãµãŒãã®èµ·åãããŸãããã æ³šæç¹ãšããŠãstdio Transportã§å®è£
ãããMCPãµãŒãã¯å
éšã§ç¶æ
ãæã€ãããå€ãã®å Žåã»ãã·ã§ã³ããšã«MCPãµãŒããèµ·åããå¿
èŠããããŸãã sessions := &sync.Map{} mux.HandleFunc( "GET /sse" , func (w http.ResponseWriter, r *http.Request) { <snip> cmd := exec.Command(name, arg...) stdin, err := cmd.StdinPipe() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } stdout, err := cmd.StdoutPipe() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err := cmd.Start(); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } go func () { scanner := bufio.NewScanner(stdout) for scanner.Scan() { s.responseQueue <- scanner.Bytes() } }() defer func () { _ = cmd.Process.Kill() _ = cmd.Wait() <snip> }() <snip> for { select { case data := <-s.requestQueue: _, _ = stdin.Write(data) case data := <-s.responseQueue: _, _ = fmt.Fprint(w, fmt.Sprintf( "event: message \n data: %s \n\n " , data)) flusher.Flush() case <-r.Context().Done(): return } } } stdio Transportã®MCPãµãŒãã®æšæºåºåã s.responseQueue ãä»ããŠMCPã¯ã©ã€ã¢ã³ãã«éç¥ããã»ãã·ã§ã³éå§æã«MCPã¯ã©ã€ã¢ã³ãã«éç¥ãããã€ããŒããåãåãããã®ãšã³ããã€ã³ãããéä¿¡ããã s.requestQueue ããåãåã£ããªã¯ãšã¹ã㯠stdin.Write(data) ã§æšæºå
¥åã«æžã蟌ãã§ããŸãã ã»ãã·ã§ã³çµäºæã«ã³ãã³ããçµäºããããšãå¿ããªãããã«ããŸãããã ãã€ããŒããåãåãããã®ãšã³ããã€ã³ãã®å®è£
mux.HandleFunc( "POST /messages" , func (w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get( "sessionId" ) if id == "" { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } sany, ok := sessions.Load(id) if !ok { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } s := sany.(*session) var rawMessage json.RawMessage if err := json.NewDecoder(r.Body).Decode(&rawMessage); err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var mcpMessage MCPMessage if err := json.Unmarshal(rawMessage, &mcpMessage); err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if verbose { log.Printf( "%s: %s" , id, mcpMessage.Method) } s.requestQueue <- rawMessage s.requestQueue <- [] byte ( " \n " ) w.Header().Set( "Content-Type" , "application/json" ) w.WriteHeader(http.StatusAccepted) _, _ = w.Write([] byte (http.StatusText(http.StatusAccepted))) }) ãã¡ãã¯ã·ã³ãã«ã§ãã¯ã©ã€ã¢ã³ãã«éç¥ããã¡ãã»ãŒãžãããšã« sessionId ãéãããŠãããããããããã»ãã·ã§ã³ãååŸããéãããŠãããã€ããŒãããã®ãŸãŸ s.requestQueue ã«æµããŠããŸãã åè¿°ã®éã s.requestQueue ã«éä¿¡ããããã€ããŒãã¯ã»ãã·ã§ã³ããšã«èµ·åããŠããã³ãã³ãã®æšæºå
¥åã«æžã蟌ãŸããŸãã ãããŸã§ã®ã·ã³ãã«ãªå®è£
ã§ãstdio Transportã®ã¿ã«å¯Ÿå¿ããMCPãµãŒããç¡çããSSE Transportã«å¯Ÿå¿ãããããšãã§ããŸããã åŸã¯ãããªæãã§äœ¿ãã°ããããã§ãã FROM ghcr.io/lifull/keel/mcp-stdio-proxy:main AS mcp-stdio-proxy FROM ghcr.io/github/github-mcp-server@sha256:fdf04e33b437c523d2f091b7bf8dc3724c88dbf9913d6568f12f0fcf48aaff95 COPY --link --from=mcp-stdio-proxy /usr/local/bin/mcp-stdio-proxy /usr/local/bin/mcp-stdio-proxy ENTRYPOINT ["/usr/local/bin/mcp-stdio-proxy", "/server/github-mcp-server", "stdio"] ãŸãšã ãã®ããã«LIFULLã§ã¯æ¯èŒçå®å
šã«MCPãµãŒããåãããŠããŸãã éæž¡æããã®æ··æ²ã®ãããªæ°ãããŸãããã»ã©ã»ã©ã®ããã¯ã§ç§©åºãæã«å
¥ããããæ°ãããŸãã SSE Transportã«ã¯è² è·åæ£ããã¥ãããšããåé¡ãããããããåããŠStreamable HTTP Transportãéçºããããªã©ãMCPã¯ãŸã é²åã®éäžã§ãã ã»ãã¥ãªãã£ã«æ°ãé
ããªãããããããMCPã䜿ã£ãŠããããã§ãã ã³ãã¥ããã£ã®å®è£
ã倧åå¢ããŠããŠããŠãæã
ãéçºããç¡éã«ã¹ã±ãŒã«ããæ±çšAI(ä»®)ã«ãMCPã¯ã©ã€ã¢ã³ããå®è£
ããŠãFunction CallingãšMCPã®ãšã³ã·ã¹ãã ãçµ±åãããã«ããšãŒãžã§ã³ãã§çã®æ±çšAIãç®æããŠããŸãã ãã©ãããã©ãŒããŒãšããŠã®LLMã«ãŸã€ããæŽ»åã«èå³ãããæ¹ãããã°ãæ¯éã«ãžã¥ã¢ã«é¢è«ãããŠãã ããïŒ hrmos.co