feat: 打通 Dispatcher→MCP 工具调用链路 (core NATS request-reply)
第 4 层 Dispatcher 经 NATS request-reply + 队列组同步调用第 5 层 MCP 工具, 工具不可用/超时即降级,不阻断主流程。 - shared/contract: ToolCall/ToolResult + sundynix.tools.go.* subject 约定 + ToolSubjectGo/Py - shared/bus: CallTool(发起) / ServeTool(队列组订阅+应答) - mcp-go: 接共享 bus,gateway 通配订阅按工具名分发(wiki_search/echo),main 优雅退出 - dispatcher: ToolCaller 接口 + Orchestrator.retrieveContext(调 wiki_search,超时3s降级) - e2e: TestToolCallRoundTrip(PASS);demo.sh 加 mcp-go(就绪门避免启动竞态),live 跑通 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,18 +2,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
||||
|
||||
"github.com/sundynix/sundynix-mcp-go/internal/mcp"
|
||||
"github.com/sundynix/sundynix-mcp-go/internal/search"
|
||||
)
|
||||
|
||||
func main() {
|
||||
engine := search.NewHybrid() // LLM Wiki 混合检索:Bleve + Milvus + Neo4j
|
||||
gw := mcp.NewGateway(engine)
|
||||
natsURL := envOr("NATS_URL", "nats://localhost:4222")
|
||||
|
||||
log.Println("[mcp_go] serving MCP over sundynix.tools.go.*")
|
||||
if err := gw.Serve(); err != nil {
|
||||
b, err := sharedbus.Connect(natsURL)
|
||||
if err != nil {
|
||||
log.Fatalf("[mcp_go] nats connect: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
log.Printf("[mcp_go] connected %s", natsURL)
|
||||
|
||||
engine := search.NewHybrid() // LLM Wiki 混合检索:Bleve + Milvus + Neo4j
|
||||
gw := mcp.NewGateway(b, engine)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
log.Println("[mcp_go] serving MCP over sundynix.tools.go.* (Ctrl-C to quit)")
|
||||
if err := gw.Serve(ctx); err != nil && err != context.Canceled {
|
||||
log.Fatalf("[mcp_go] exit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
+11
-4
@@ -2,9 +2,16 @@ module github.com/sundynix/sundynix-mcp-go
|
||||
|
||||
go 1.23
|
||||
|
||||
require github.com/sundynix/sundynix-shared v0.0.0
|
||||
|
||||
require (
|
||||
github.com/blevesearch/bleve/v2 v2.4.2
|
||||
github.com/milvus-io/milvus-sdk-go/v2 v2.4.1
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.24.0
|
||||
github.com/nats-io/nats.go v1.37.0
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/nats-io/nats.go v1.37.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.7 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/sundynix/sundynix-shared => ../sundynix-shared
|
||||
|
||||
+21
-3
@@ -1,4 +1,22 @@
|
||||
github.com/blevesearch/bleve/v2 v2.4.2/go.mod h1:ATNKj7Yl2oJv/lGuF4kx39bST2dveX6w0th2FFYLkc8=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/nats-io/jwt/v2 v2.5.8 h1:uvdSzwWiEGWGXf+0Q+70qv6AQdvcvxrv9hPM0RiPamE=
|
||||
github.com/nats-io/jwt/v2 v2.5.8/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A=
|
||||
github.com/nats-io/nats-server/v2 v2.10.20 h1:CXDTYNHeBiAKBTAIP2gjpgbWap2GhATnTLgP8etyvEI=
|
||||
github.com/nats-io/nats-server/v2 v2.10.20/go.mod h1:hgcPnoUtMfxz1qVOvLZGurVypQ+Cg6GXVXjG53iHk+M=
|
||||
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
|
||||
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.24.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||
github.com/qdrant/go-client v1.11.0/go.mod h1:j+OVRsJIZhOSRK2toPl8tTBOhwr4AxXCz9RACzv0JB4=
|
||||
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
|
||||
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
|
||||
@@ -1,18 +1,71 @@
|
||||
// Package mcp 实现 MCP 协议网关,把工具注册到 NATS 并响应调用。
|
||||
package mcp
|
||||
|
||||
import "github.com/sundynix/sundynix-mcp-go/internal/search"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
// Gateway 暴露 MCP 协议端点(stdio / HTTP / NATS)。
|
||||
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
|
||||
"github.com/sundynix/sundynix-mcp-go/internal/search"
|
||||
)
|
||||
|
||||
// Gateway 暴露 MCP 协议端点,经共享 bus 订阅 sundynix.tools.go.* 响应调用。
|
||||
type Gateway struct {
|
||||
bus *sharedbus.Bus
|
||||
search *search.Hybrid
|
||||
}
|
||||
|
||||
func NewGateway(s *search.Hybrid) *Gateway { return &Gateway{search: s} }
|
||||
|
||||
// Serve 监听 sundynix.tools.go.* 并按 MCP 协议分发工具调用。
|
||||
func (g *Gateway) Serve() error {
|
||||
// TODO: 注册工具清单 (wiki_search / render_doc / call_external_api ...)
|
||||
// 订阅 NATS,按 MCP JSON-RPC 解析并路由
|
||||
select {}
|
||||
func NewGateway(b *sharedbus.Bus, s *search.Hybrid) *Gateway {
|
||||
return &Gateway{bus: b, search: s}
|
||||
}
|
||||
|
||||
// Serve 以队列组通配订阅 sundynix.tools.go.>,按工具名分发并阻塞。
|
||||
func (g *Gateway) Serve(ctx context.Context) error {
|
||||
unsub, err := g.bus.ServeTool(contract.SubjectToolsGoAll, contract.QueueToolsGo, g.dispatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = unsub() }()
|
||||
log.Printf("[mcp_go] tools ready on %s (queue=%s): wiki_search, echo",
|
||||
contract.SubjectToolsGoAll, contract.QueueToolsGo)
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// dispatch 按 ToolCall.Tool 路由到具体工具实现。
|
||||
func (g *Gateway) dispatch(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
||||
log.Printf("[mcp_go] tool=%s task=%s args=%v", call.Tool, call.TaskID, call.Args)
|
||||
switch call.Tool {
|
||||
case "wiki_search":
|
||||
return g.wikiSearch(ctx, call)
|
||||
case "echo":
|
||||
return &contract.ToolResult{OK: true, Content: fmt.Sprint(call.Args["text"])}
|
||||
default:
|
||||
return &contract.ToolResult{OK: false, Error: "unknown tool: " + call.Tool}
|
||||
}
|
||||
}
|
||||
|
||||
// wikiSearch 调 Hybrid 混合检索引擎。引擎目前为桩(返回空),
|
||||
// 这里仍把调用链路做真:真实接入 Bleve/Milvus/Neo4j 后无需改动协议。
|
||||
func (g *Gateway) wikiSearch(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
|
||||
q, _ := call.Args["q"].(string)
|
||||
results, err := g.search.Query(ctx, q, 5)
|
||||
if err != nil {
|
||||
return &contract.ToolResult{OK: false, Error: "wiki_search: " + err.Error()}
|
||||
}
|
||||
return &contract.ToolResult{
|
||||
OK: true,
|
||||
Content: fmt.Sprintf("[wiki_search] 命中 %d 条(Bleve+Milvus+Neo4j 混合检索桩)查询=%q", len(results), preview(q)),
|
||||
}
|
||||
}
|
||||
|
||||
func preview(s string) string {
|
||||
r := []rune(s)
|
||||
if len(r) > 40 {
|
||||
return string(r[:40]) + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user