feat: MCP Streamable HTTP server with full tool set
Wire go-sdk Streamable HTTP at /mcp with stamp, verify, hash_report, signup, login, session, create_user. Route global/CID to VeraeTime and local|shared to FileLedger; resolve Authorization bearer via authctx. Flags/env for transport, host, port, require-bearer, URLs, ledger path.
This commit is contained in:
parent
793746fcd0
commit
d379931bd1
1 changed files with 925 additions and 6 deletions
107
internal/server/server_test.go
Normal file
107
internal/server/server_test.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package server_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/ledger"
|
||||
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/server"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
const sha256Empty = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
|
||||
func TestStreamableHTTP_InitializeListStampLocal(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
led, err := ledger.Open(filepath.Join(dir, "ledger.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
authPath := filepath.Join(dir, "auth.properties")
|
||||
if err := os.WriteFile(authPath, []byte("username=x\npassword=y\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
srv, err := server.New(server.Options{
|
||||
Ledger: led,
|
||||
AuthPath: authPath,
|
||||
GlobalURL: "http://127.0.0.1:1", // unused for local stamp
|
||||
RequireBearer: false,
|
||||
JSONResponse: true,
|
||||
Stateless: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
httpServer := httptest.NewServer(srv.Handler())
|
||||
t.Cleanup(httpServer.Close)
|
||||
|
||||
ctx := context.Background()
|
||||
client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "0.0.1"}, nil)
|
||||
session, err := client.Connect(ctx, &mcp.StreamableClientTransport{Endpoint: httpServer.URL + "/mcp"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = session.Close() })
|
||||
|
||||
tools, err := session.ListTools(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tools/list: %v", err)
|
||||
}
|
||||
wantTools := []string{"stamp", "verify", "hash_report", "signup", "login", "session", "create_user"}
|
||||
got := map[string]bool{}
|
||||
for _, tool := range tools.Tools {
|
||||
got[tool.Name] = true
|
||||
}
|
||||
for _, name := range wantTools {
|
||||
if !got[name] {
|
||||
t.Fatalf("tools/list missing %q; got %#v", name, got)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := session.CallTool(ctx, &mcp.CallToolParams{
|
||||
Name: "stamp",
|
||||
Arguments: map[string]any{
|
||||
"sha256": sha256Empty,
|
||||
"scope": "local",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("tools/call stamp: %v", err)
|
||||
}
|
||||
if res.IsError {
|
||||
t.Fatalf("stamp tool error: %+v", res.Content)
|
||||
}
|
||||
raw, err := json.Marshal(res.StructuredContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out struct {
|
||||
Receipts []struct {
|
||||
Hash string `json:"hash"`
|
||||
Scope string `json:"scope"`
|
||||
} `json:"receipts"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
t.Fatalf("decode structured: %v raw=%s", err, raw)
|
||||
}
|
||||
if len(out.Receipts) != 1 {
|
||||
t.Fatalf("want 1 receipt, got %s", raw)
|
||||
}
|
||||
if out.Receipts[0].Hash != sha256Empty {
|
||||
t.Fatalf("hash=%q", out.Receipts[0].Hash)
|
||||
}
|
||||
if out.Receipts[0].Scope != "local" {
|
||||
t.Fatalf("scope=%q", out.Receipts[0].Scope)
|
||||
}
|
||||
if !strings.Contains(string(raw), "receipt_id") {
|
||||
t.Fatalf("missing receipt_id in %s", raw)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue