# Directory Structure ``` ├── .gitignore ├── client │ └── client.go ├── go.mod ├── go.sum ├── main.go ├── Makefile ├── mcp.go ├── README.md └── weaviate.go ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | */mcp-server 2 | */.idea/* 3 | 4 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Weaviate MCP Server 2 | 3 | ## Instructions 4 | 5 | Build the server: 6 | ``` 7 | make build 8 | ``` 9 | 10 | Run the test client 11 | ``` 12 | make run-client 13 | ``` 14 | 15 | ## Tools 16 | 17 | ### Insert One 18 | Insert an object into weaviate. 19 | 20 | **Request body:** 21 | ```json 22 | {} 23 | ``` 24 | 25 | **Response body** 26 | ```json 27 | {} 28 | ``` 29 | 30 | ### Query 31 | Retrieve objects from weaviate with hybrid search. 32 | 33 | **Request body:** 34 | ```json 35 | {} 36 | ``` 37 | 38 | **Response body** 39 | ```json 40 | {} 41 | ``` 42 | ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func main() { 8 | // TODO: get all WeaviateConn config from env 9 | // TODO: support SSEs 10 | // var transport string 11 | // flag.StringVar(&transport, "transport", "stdio", "Specifies the transport protocol. One of [stdio|sse]") 12 | s, err := NewMCPServer() 13 | if err != nil { 14 | log.Fatalf("failed to start mcp server: %v", err) 15 | } 16 | _ = s 17 | s.Serve() 18 | } 19 | ``` -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/mark3labs/mcp-go/client" 9 | "github.com/mark3labs/mcp-go/mcp" 10 | ) 11 | 12 | func main() { 13 | ctx := context.Background() 14 | cmd := "./mcp-server" 15 | 16 | c, err := newMCPClient(ctx, cmd) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | defer c.Close() 21 | 22 | { 23 | insertRes, err := insertRequest(ctx, c) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | log.Printf("insert-one response: %+v", insertRes) 28 | } 29 | { 30 | queryRes, err := queryRequest(ctx, c) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | log.Printf("query response: %+v", queryRes) 35 | } 36 | } 37 | 38 | func newMCPClient(ctx context.Context, cmd string) (*client.Client, error) { 39 | c, _ := client.NewStdioMCPClient(cmd, nil) 40 | initRes, err := c.Initialize(ctx, mcp.InitializeRequest{}) 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to init client: %w", err) 43 | } 44 | log.Printf("init result: %+v", initRes) 45 | if err := c.Start(ctx); err != nil { 46 | return nil, fmt.Errorf("failed to start client: %w", err) 47 | } 48 | if err := c.Ping(ctx); err != nil { 49 | return nil, fmt.Errorf("failed to ping server: %w", err) 50 | } 51 | return c, nil 52 | } 53 | 54 | func insertRequest(ctx context.Context, c *client.Client) (*mcp.CallToolResult, error) { 55 | request := mcp.CallToolRequest{} 56 | request.Params.Name = "weaviate-insert-one" 57 | request.Params.Arguments = map[string]interface{}{ 58 | "collection": "WorldMap", 59 | "properties": map[string]interface{}{ 60 | "continent": "Europe", 61 | "country": "Spain", 62 | "city": "Valencia", 63 | }, 64 | } 65 | log.Printf("insert request: %+v", request) 66 | res, err := c.CallTool(ctx, request) 67 | if err != nil { 68 | return nil, fmt.Errorf("failed to call insert-one tool: %v", err) 69 | } 70 | return res, nil 71 | } 72 | 73 | func queryRequest(ctx context.Context, c *client.Client) (*mcp.CallToolResult, error) { 74 | request := mcp.CallToolRequest{} 75 | request.Params.Name = "weaviate-query" 76 | request.Params.Arguments = map[string]interface{}{ 77 | "collection": "WorldMap", 78 | "query": "What country is Valencia in?", 79 | "targetProperties": []string{"continent", "country", "city"}, 80 | } 81 | log.Printf("query request: %+v", request) 82 | res, err := c.CallTool(ctx, request) 83 | if err != nil { 84 | return nil, fmt.Errorf("failed to call query tool: %v", err) 85 | } 86 | return res, nil 87 | } 88 | ``` -------------------------------------------------------------------------------- /weaviate.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/weaviate/weaviate-go-client/v4/weaviate" 11 | "github.com/weaviate/weaviate-go-client/v4/weaviate/graphql" 12 | "github.com/weaviate/weaviate/entities/models" 13 | ) 14 | 15 | type WeaviateConnection struct { 16 | client *weaviate.Client 17 | } 18 | 19 | func NewWeaviateConnection() (*WeaviateConnection, error) { 20 | // TODO: get config from env 21 | client, err := weaviate.NewClient(weaviate.Config{ 22 | Host: "localhost:8080", 23 | Scheme: "http", 24 | StartupTimeout: time.Second, 25 | }) 26 | if err != nil { 27 | return nil, fmt.Errorf("connect to weaviate: %w", err) 28 | } 29 | return &WeaviateConnection{client}, nil 30 | } 31 | 32 | func (conn *WeaviateConnection) InsertOne(ctx context.Context, 33 | collection string, props interface{}, 34 | ) (*models.Object, error) { 35 | obj := models.Object{ 36 | Class: collection, 37 | Properties: props, 38 | } 39 | // Use batch to leverage autoschema and gRPC 40 | resp, err := conn.batchInsert(ctx, &obj) 41 | if err != nil { 42 | return nil, fmt.Errorf("insert one object: %w", err) 43 | } 44 | 45 | return &resp[0].Object, err 46 | } 47 | 48 | func (conn *WeaviateConnection) Query(ctx context.Context, collection, 49 | query string, targetProps []string, 50 | ) (string, error) { 51 | hybrid := graphql.HybridArgumentBuilder{} 52 | hybrid.WithQuery(query) 53 | res, err := conn.client.GraphQL().Get(). 54 | WithClassName(collection).WithHybrid(&hybrid). 55 | WithFields(func() []graphql.Field { 56 | fields := make([]graphql.Field, len(targetProps)) 57 | for i, prop := range targetProps { 58 | fields[i] = graphql.Field{Name: prop} 59 | } 60 | return fields 61 | }()...). 62 | Do(context.Background()) 63 | if err != nil { 64 | return "", err 65 | } 66 | b, err := json.Marshal(res) 67 | if err != nil { 68 | return "", fmt.Errorf("unmarshal query response: %w", err) 69 | } 70 | return string(b), nil 71 | } 72 | 73 | func (conn *WeaviateConnection) batchInsert(ctx context.Context, objs ...*models.Object) ([]models.ObjectsGetResponse, error) { 74 | resp, err := conn.client.Batch().ObjectsBatcher().WithObjects(objs...).Do(ctx) 75 | if err != nil { 76 | return nil, fmt.Errorf("make insertion request: %w", err) 77 | } 78 | for _, res := range resp { 79 | if res.Result != nil && res.Result.Errors != nil && res.Result.Errors.Error != nil { 80 | for _, nestedErr := range res.Result.Errors.Error { 81 | err = errors.Join(err, errors.New(nestedErr.Message)) 82 | } 83 | } 84 | } 85 | 86 | return resp, err 87 | } 88 | ``` -------------------------------------------------------------------------------- /mcp.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mark3labs/mcp-go/mcp" 7 | "github.com/mark3labs/mcp-go/server" 8 | ) 9 | 10 | type MCPServer struct { 11 | server *server.MCPServer 12 | weaviateConn *WeaviateConnection 13 | defaultCollection string 14 | } 15 | 16 | func NewMCPServer() (*MCPServer, error) { 17 | conn, err := NewWeaviateConnection() 18 | if err != nil { 19 | return nil, err 20 | } 21 | s := &MCPServer{ 22 | server: server.NewMCPServer( 23 | "Weaviate MCP Server", 24 | "0.1.0", 25 | server.WithToolCapabilities(true), 26 | server.WithPromptCapabilities(true), 27 | server.WithResourceCapabilities(true, true), 28 | server.WithRecovery(), 29 | ), 30 | weaviateConn: conn, 31 | // TODO: configurable collection name 32 | defaultCollection: "DefaultCollection", 33 | } 34 | s.registerTools() 35 | return s, nil 36 | } 37 | 38 | func (s *MCPServer) Serve() { 39 | server.ServeStdio(s.server) 40 | } 41 | 42 | func (s *MCPServer) registerTools() { 43 | insertOne := mcp.NewTool( 44 | "weaviate-insert-one", 45 | mcp.WithString( 46 | "collection", 47 | mcp.Description("Name of the target collection"), 48 | ), 49 | mcp.WithObject( 50 | "properties", 51 | mcp.Description("Object properties to insert"), 52 | mcp.Required(), 53 | ), 54 | ) 55 | query := mcp.NewTool( 56 | "weaviate-query", 57 | mcp.WithString( 58 | "query", 59 | mcp.Description("Query data within Weaviate"), 60 | mcp.Required(), 61 | ), 62 | mcp.WithArray( 63 | "targetProperties", 64 | mcp.Description("Properties to return with the query"), 65 | mcp.Required(), 66 | ), 67 | ) 68 | 69 | s.server.AddTools( 70 | server.ServerTool{Tool: insertOne, Handler: s.weaviateInsertOne}, 71 | server.ServerTool{Tool: query, Handler: s.weaviateQuery}, 72 | ) 73 | } 74 | 75 | func (s *MCPServer) weaviateInsertOne(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { 76 | targetCol := s.parseTargetCollection(req) 77 | props := req.Params.Arguments["properties"].(map[string]interface{}) 78 | 79 | res, err := s.weaviateConn.InsertOne(context.Background(), targetCol, props) 80 | if err != nil { 81 | return mcp.NewToolResultErrorFromErr("failed to insert object", err), nil 82 | } 83 | return mcp.NewToolResultText(res.ID.String()), nil 84 | } 85 | 86 | func (s *MCPServer) weaviateQuery(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { 87 | targetCol := s.parseTargetCollection(req) 88 | query := req.Params.Arguments["query"].(string) 89 | // TODO: how to enforce `Required` within the sdk so we don't have to validate here 90 | props := req.Params.Arguments["targetProperties"].([]interface{}) 91 | var targetProps []string 92 | { 93 | for _, prop := range props { 94 | typed, ok := prop.(string) 95 | if !ok { 96 | return mcp.NewToolResultError("targetProperties must contain only strings"), nil 97 | } 98 | targetProps = append(targetProps, typed) 99 | } 100 | } 101 | res, err := s.weaviateConn.Query(context.Background(), targetCol, query, targetProps) 102 | if err != nil { 103 | return mcp.NewToolResultErrorFromErr("failed to process query", err), nil 104 | } 105 | return mcp.NewToolResultText(res), nil 106 | } 107 | 108 | func (s *MCPServer) parseTargetCollection(req mcp.CallToolRequest) string { 109 | var ( 110 | targetCol = s.defaultCollection 111 | ) 112 | col, ok := req.Params.Arguments["collection"].(string) 113 | if ok { 114 | targetCol = col 115 | } 116 | return targetCol 117 | } 118 | ```