feat(ingestion): implement brain_query MCP tool
Wraps the existing search.Query function. Same BM25 over brain/knowledge/ and brain/wiki/ that the HTTP /query endpoint serves. Plan note: handleCall switch replaces the single-line stub from Task 1 — no unknownToolError type to remove since Task 1 inlined the error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
package mcp
|
package mcp
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mathiasbq/hyperguild/ingestion/internal/search"
|
||||||
|
)
|
||||||
|
|
||||||
// tools returns the tool descriptors. Handler bodies for each tool are filled
|
// tools returns the tool descriptors. Handler bodies for each tool are filled
|
||||||
// in subsequent tasks; this file currently only provides the descriptors.
|
// in subsequent tasks; this file currently only provides the descriptors.
|
||||||
@@ -73,3 +79,26 @@ func (s *Server) tools() []map[string]any {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type brainQueryArgs struct {
|
||||||
|
Query string `json:"query"`
|
||||||
|
Limit int `json:"limit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) brainQuery(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
||||||
|
var a brainQueryArgs
|
||||||
|
if err := json.Unmarshal(args, &a); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse args: %w", err)
|
||||||
|
}
|
||||||
|
if a.Query == "" {
|
||||||
|
return nil, fmt.Errorf("query is required")
|
||||||
|
}
|
||||||
|
if a.Limit == 0 {
|
||||||
|
a.Limit = 5
|
||||||
|
}
|
||||||
|
results, err := search.Query(s.brainDir, a.Query, a.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("search: %w", err)
|
||||||
|
}
|
||||||
|
return json.Marshal(map[string]any{"results": results})
|
||||||
|
}
|
||||||
|
|||||||
52
ingestion/internal/mcp/handlers_test.go
Normal file
52
ingestion/internal/mcp/handlers_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package mcp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mathiasbq/hyperguild/ingestion/internal/mcp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toolCall(t *testing.T, srv http.Handler, name string, args map[string]any) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
bodyBytes, err := json.Marshal(map[string]any{
|
||||||
|
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
||||||
|
"params": map[string]any{"name": name, "arguments": args},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(bodyBytes))
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, req)
|
||||||
|
require.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBrainQueryReturnsResults(t *testing.T) {
|
||||||
|
brainDir := t.TempDir()
|
||||||
|
knowledge := filepath.Join(brainDir, "knowledge")
|
||||||
|
require.NoError(t, os.MkdirAll(knowledge, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(
|
||||||
|
filepath.Join(knowledge, "tdd.md"),
|
||||||
|
[]byte("# TDD\n\nTest-driven development is iterative.\n"),
|
||||||
|
0o644,
|
||||||
|
))
|
||||||
|
|
||||||
|
srv := mcp.NewServer(brainDir, nil, nil)
|
||||||
|
resp := toolCall(t, srv, "brain_query", map[string]any{"query": "tdd"})
|
||||||
|
|
||||||
|
require.Nil(t, resp["error"])
|
||||||
|
result := resp["result"].(map[string]any)
|
||||||
|
content := result["content"].([]any)
|
||||||
|
require.NotEmpty(t, content)
|
||||||
|
text := content[0].(map[string]any)["text"].(string)
|
||||||
|
assert.Contains(t, text, "tdd.md")
|
||||||
|
}
|
||||||
@@ -113,7 +113,12 @@ func writeError(w http.ResponseWriter, id any, code int, msg string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCall dispatches a tools/call. Stub for Task 1; expanded in later tasks.
|
// handleCall dispatches a tools/call to the appropriate tool handler.
|
||||||
func (s *Server) handleCall(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error) {
|
func (s *Server) handleCall(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error) {
|
||||||
|
switch name {
|
||||||
|
case "brain_query":
|
||||||
|
return s.brainQuery(ctx, args)
|
||||||
|
default:
|
||||||
return nil, fmt.Errorf("unknown tool: %s", name)
|
return nil, fmt.Errorf("unknown tool: %s", name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user