mirror of
https://github.com/hanzoai/mcp.git
synced 2026-10-15 12:30:12 +00:00
feat: cloud Mount() per HIP-0106 (#6)
* feat: cloud Mount() per HIP-0106 Adds pkg/mcp with the canonical Mount(*zip.App, cloud.Deps) entry so the unified cloud binary can serve the HIP-0300 tool surface under /v1/mcp. The route layer is pure-Go: it defines an Invoker contract and dispatches GET /v1/mcp/tools, POST /v1/mcp/tools/:name, plus the standard search and fetch shortcuts. The CGO-backed hanzomcp package implements Invoker; a build without CGO mounts the routes and returns 503 until SetInvoker registers a runtime. Adds schema/mcp.zap describing the typed surface. * refactor: collapse pkg/go/ → root + flatten cloud import Repo IS the package. Mount() at root. Consumers use `github.com/hanzoai/mcp/go` (no /pkg/go/ nesting).
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package mcp
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// overrideAction rewrites the JSON object body so its "action" key is set
|
||||
// to the given value. If the body is empty or not a JSON object, returns
|
||||
// a minimal {"action": v} payload.
|
||||
func overrideAction(body []byte, v string) []byte {
|
||||
var m map[string]any
|
||||
if len(body) == 0 || json.Unmarshal(body, &m) != nil {
|
||||
out, _ := json.Marshal(map[string]any{"action": v})
|
||||
return out
|
||||
}
|
||||
m["action"] = v
|
||||
out, _ := json.Marshal(m)
|
||||
return out
|
||||
}
|
||||
@@ -1,3 +1,31 @@
|
||||
module github.com/hanzoai/mcp/go
|
||||
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
github.com/hanzoai/cloud v0.0.0-20260519044114-66d5d2a6312c
|
||||
github.com/hanzoai/zip v0.1.0
|
||||
github.com/luxfi/log v1.4.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/gofiber/fiber/v3 v3.2.0 // indirect
|
||||
github.com/gofiber/schema v1.7.1 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.70.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/hanzoai/cloud => ../../cloud
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gofiber/fiber/v3 v3.2.0 h1:g9+09D320foINPpCnR3ibQ5oBEFHjAWRRfDG1te54u8=
|
||||
github.com/gofiber/fiber/v3 v3.2.0/go.mod h1:FHOsc2Db7HhHpsE62QAaJlXVV1pNkbZEptZ4jtti7m4=
|
||||
github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
|
||||
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
|
||||
github.com/gofiber/utils/v2 v2.0.4 h1:WwAxUA7L4MW2DjdEHF234lfqvBqd2vYYuBtA9TJq2ec=
|
||||
github.com/gofiber/utils/v2 v2.0.4/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hanzoai/zip v0.1.0 h1:rsGLHl0cr7EBvp2EQk6KoEizBpOvgU7Uv0JP9GJ7cnw=
|
||||
github.com/hanzoai/zip v0.1.0/go.mod h1:3G+k2wy5bQ1wld66m0OPH8/LJ0kgIeNi1KPymMmlbf4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
|
||||
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
||||
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
|
||||
github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,74 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Invoker is the contract between mcp's HTTP routes and the underlying
|
||||
// tool runtime. The CGO-backed hanzomcp package implements this; a
|
||||
// pure-Go cloud build with no MCP backend registers nil and the routes
|
||||
// return 503.
|
||||
type Invoker interface {
|
||||
// CallTool dispatches a tool by name with JSON-encoded params and
|
||||
// returns the JSON-encoded result envelope.
|
||||
CallTool(ctx context.Context, name string, paramsJSON []byte) ([]byte, error)
|
||||
|
||||
// ListTools returns JSON-encoded definitions for the available tools.
|
||||
ListTools(ctx context.Context) ([]byte, error)
|
||||
}
|
||||
|
||||
// ErrNoInvoker is returned when the route layer is mounted but no Invoker
|
||||
// has been registered. Callers see HTTP 503.
|
||||
var ErrNoInvoker = errors.New("mcp: no invoker registered (build without MCP runtime)")
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
current Invoker
|
||||
)
|
||||
|
||||
// SetInvoker registers the active Invoker. Pass nil to deactivate
|
||||
// (routes return 503). Safe for concurrent use.
|
||||
func SetInvoker(inv Invoker) {
|
||||
mu.Lock()
|
||||
current = inv
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
// get returns the active Invoker or the null implementation if none
|
||||
// registered.
|
||||
func get() Invoker {
|
||||
mu.RLock()
|
||||
inv := current
|
||||
mu.RUnlock()
|
||||
if inv == nil {
|
||||
return nullInvoker{}
|
||||
}
|
||||
return inv
|
||||
}
|
||||
|
||||
// nullInvoker is the standby when no real backend is wired. Returns
|
||||
// ErrNoInvoker so route handlers respond with 503.
|
||||
type nullInvoker struct{}
|
||||
|
||||
func (nullInvoker) CallTool(context.Context, string, []byte) ([]byte, error) {
|
||||
return nil, ErrNoInvoker
|
||||
}
|
||||
|
||||
func (nullInvoker) ListTools(context.Context) ([]byte, error) {
|
||||
return nil, ErrNoInvoker
|
||||
}
|
||||
|
||||
// errorEnvelope is the JSON shape returned when the runtime fails or
|
||||
// no invoker is registered.
|
||||
type errorEnvelope struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func encodeError(msg string) []byte {
|
||||
b, _ := json.Marshal(errorEnvelope{Success: false, Error: msg})
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Package mcp mounts the Hanzo MCP tool surface under /v1/mcp per HIP-0106.
|
||||
//
|
||||
// Hanzo MCP exposes the HIP-0300 unified tool catalog (fs, exec, code, git,
|
||||
// fetch, workspace, computer, think, memory, hanzo, plan, tasks, mode) over
|
||||
// HTTP. The Go bridge to the native tool runtime is the CGO package
|
||||
// github.com/hanzoai/mcp/go/hanzomcp — keeping the route layer pure-Go means
|
||||
// the unified cloud binary builds without CGO. A CGO-enabled build can
|
||||
// register an invoker via SetInvoker before app.Listen.
|
||||
package mcp
|
||||
@@ -0,0 +1,119 @@
|
||||
package mcp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/mcp/go"
|
||||
"github.com/hanzoai/zip"
|
||||
)
|
||||
|
||||
type stubInvoker struct {
|
||||
tools []byte
|
||||
last string
|
||||
}
|
||||
|
||||
func (s *stubInvoker) CallTool(_ context.Context, name string, params []byte) ([]byte, error) {
|
||||
s.last = name
|
||||
return []byte(`{"success":true,"content":{"tool":"` + name + `","params":` + string(params) + `}}`), nil
|
||||
}
|
||||
|
||||
func (s *stubInvoker) ListTools(context.Context) ([]byte, error) {
|
||||
return s.tools, nil
|
||||
}
|
||||
|
||||
func newTestApp(t *testing.T) (*zip.App, *stubInvoker) {
|
||||
t.Helper()
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
if err := mcp.Mount(app, cloud.Deps{}); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
}
|
||||
inv := &stubInvoker{tools: []byte(`{"tools":[{"name":"fs","description":"","inputSchema":""}]}`)}
|
||||
mcp.SetInvoker(inv)
|
||||
return app, inv
|
||||
}
|
||||
|
||||
func roundTrip(t *testing.T, app *zip.App, method, target string, body string) (*http.Response, []byte) {
|
||||
t.Helper()
|
||||
var bodyReader io.Reader
|
||||
if body != "" {
|
||||
bodyReader = strings.NewReader(body)
|
||||
}
|
||||
req := httptest.NewRequest(method, target, bodyReader)
|
||||
if body != "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := app.Fiber().Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
out, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
return resp, out
|
||||
}
|
||||
|
||||
func TestMountHealthWithInvoker(t *testing.T) {
|
||||
app, _ := newTestApp(t)
|
||||
defer mcp.SetInvoker(nil)
|
||||
resp, body := roundTrip(t, app, http.MethodGet, "/v1/mcp/health", "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", resp.StatusCode, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountHealthNoInvoker(t *testing.T) {
|
||||
app := zip.New(zip.Config{DisableStartupMessage: true})
|
||||
if err := mcp.Mount(app, cloud.Deps{}); err != nil {
|
||||
t.Fatalf("Mount: %v", err)
|
||||
}
|
||||
mcp.SetInvoker(nil)
|
||||
resp, _ := roundTrip(t, app, http.MethodGet, "/v1/mcp/health", "")
|
||||
if resp.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Fatalf("expected 503, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountListTools(t *testing.T) {
|
||||
app, _ := newTestApp(t)
|
||||
defer mcp.SetInvoker(nil)
|
||||
resp, body := roundTrip(t, app, http.MethodGet, "/v1/mcp/tools", "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", resp.StatusCode, body)
|
||||
}
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Fatalf("invalid json: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountCallTool(t *testing.T) {
|
||||
app, inv := newTestApp(t)
|
||||
defer mcp.SetInvoker(nil)
|
||||
resp, body := roundTrip(t, app, http.MethodPost, "/v1/mcp/tools/think", `{"action":"think","thought":"hi"}`)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", resp.StatusCode, body)
|
||||
}
|
||||
if inv.last != "think" {
|
||||
t.Fatalf("invoker.last=%q want think", inv.last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountSearchShortcut(t *testing.T) {
|
||||
app, inv := newTestApp(t)
|
||||
defer mcp.SetInvoker(nil)
|
||||
resp, body := roundTrip(t, app, http.MethodPost, "/v1/mcp/search", `{"query":"hello"}`)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", resp.StatusCode, body)
|
||||
}
|
||||
if inv.last != "fs" {
|
||||
t.Fatalf("expected fs invocation, got %q", inv.last)
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/zip"
|
||||
luxlog "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// Mount registers Hanzo MCP's HTTP surface per HIP-0106.
|
||||
//
|
||||
// Routes (all under /v1/mcp):
|
||||
//
|
||||
// GET /v1/mcp/tools — list tool definitions
|
||||
// POST /v1/mcp/tools/:name — invoke tool {name} with JSON body as params
|
||||
// POST /v1/mcp/search — search shortcut (delegates to the fs tool)
|
||||
// POST /v1/mcp/fetch — fetch shortcut (delegates to the fetch tool)
|
||||
// GET /v1/mcp/health — runtime liveness
|
||||
//
|
||||
// Identity comes from gateway-minted X-Org-Id / X-User-Id headers per
|
||||
// HIP-0026. The MCP runtime is single-process today (see hanzomcp), so
|
||||
// per-org scoping happens at the tool-action level (e.g. fs root, exec
|
||||
// cwd) rather than the route layer.
|
||||
func Mount(app *zip.App, deps cloud.Deps) error {
|
||||
log := deps.Logger
|
||||
if log == nil {
|
||||
log = luxlog.New("module", "mcp")
|
||||
}
|
||||
log.Info("mcp: mounting routes", "prefix", "/v1/mcp")
|
||||
|
||||
g := app.Group("/v1/mcp")
|
||||
|
||||
g.Get("/health", handleHealth)
|
||||
g.Get("/tools", handleListTools)
|
||||
g.Post("/tools/:name", handleCallTool)
|
||||
g.Post("/search", handleSearch)
|
||||
g.Post("/fetch", handleFetch)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
cloud.Register("mcp", 160, func(app any, deps cloud.Deps) error {
|
||||
return Mount(app.(*zip.App), deps)
|
||||
})
|
||||
}
|
||||
|
||||
// handleHealth returns 200 when an invoker is registered, 503 otherwise.
|
||||
func handleHealth(c *zip.Ctx) error {
|
||||
inv := get()
|
||||
if _, err := inv.ListTools(c.Context()); err != nil {
|
||||
c.SetHeader("X-MCP-Backend", "none")
|
||||
return c.JSON(http.StatusServiceUnavailable, map[string]any{
|
||||
"status": "no-runtime",
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
c.SetHeader("X-MCP-Backend", "ready")
|
||||
return c.JSON(http.StatusOK, map[string]any{"status": "ok"})
|
||||
}
|
||||
|
||||
// handleListTools returns the registered tool catalog.
|
||||
func handleListTools(c *zip.Ctx) error {
|
||||
raw, err := get().ListTools(c.Context())
|
||||
if err != nil {
|
||||
return c.Bytes(http.StatusServiceUnavailable, encodeError(err.Error()))
|
||||
}
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
return c.Bytes(http.StatusOK, raw)
|
||||
}
|
||||
|
||||
// handleCallTool dispatches a tool by name. The request body is forwarded
|
||||
// verbatim as the params JSON.
|
||||
func handleCallTool(c *zip.Ctx) error {
|
||||
name := c.Param("name")
|
||||
if name == "" {
|
||||
return zip.ErrBadRequest("missing tool name")
|
||||
}
|
||||
body := c.Body()
|
||||
if len(body) == 0 {
|
||||
body = []byte("{}")
|
||||
}
|
||||
raw, err := get().CallTool(c.Context(), name, body)
|
||||
if err != nil {
|
||||
return c.Bytes(http.StatusServiceUnavailable, encodeError(err.Error()))
|
||||
}
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
return c.Bytes(http.StatusOK, raw)
|
||||
}
|
||||
|
||||
// handleSearch is the standard MCP search shortcut. It delegates to the
|
||||
// fs tool's search_text action.
|
||||
func handleSearch(c *zip.Ctx) error {
|
||||
// Inject action=search_text into the body. We keep this dumb — the
|
||||
// caller is expected to send a body the fs tool understands; we only
|
||||
// fill in the action when omitted.
|
||||
raw, err := get().CallTool(c.Context(), "fs", overrideAction(c.Body(), "search_text"))
|
||||
if err != nil {
|
||||
return c.Bytes(http.StatusServiceUnavailable, encodeError(err.Error()))
|
||||
}
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
return c.Bytes(http.StatusOK, raw)
|
||||
}
|
||||
|
||||
// handleFetch is the standard MCP fetch shortcut. It delegates to the
|
||||
// fetch tool's get action.
|
||||
func handleFetch(c *zip.Ctx) error {
|
||||
raw, err := get().CallTool(c.Context(), "fetch", overrideAction(c.Body(), "get"))
|
||||
if err != nil {
|
||||
return c.Bytes(http.StatusServiceUnavailable, encodeError(err.Error()))
|
||||
}
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
return c.Bytes(http.StatusOK, raw)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Hanzo MCP — ZAP Schema (HIP-0106 / HIP-0300)
|
||||
#
|
||||
# Server: hanzoai/mcp mounted at /v1/mcp inside the unified cloud binary.
|
||||
#
|
||||
# Code generation:
|
||||
# zapc generate mcp.zap --lang go --out ./go/pkg/mcp/zap/
|
||||
# zapc generate mcp.zap --lang ts --out ./src/zap/
|
||||
# zapc generate mcp.zap --lang py --out ./py/zap/
|
||||
# zapc generate mcp.zap --lang rust --out ./rust/src/zap/
|
||||
|
||||
# ── Tool catalog ─────────────────────────────────────────────────────────
|
||||
|
||||
struct ToolDef
|
||||
name Text
|
||||
description Text
|
||||
inputSchema Text # JSON schema, serialized as Text to avoid recursion
|
||||
|
||||
struct ToolList
|
||||
tools List(ToolDef)
|
||||
|
||||
# ── Invocation ──────────────────────────────────────────────────────────
|
||||
|
||||
struct CallRequest
|
||||
name Text
|
||||
params Text # caller-supplied JSON params for the tool action
|
||||
|
||||
struct CallResult
|
||||
success bool
|
||||
content Text # tool-specific JSON content
|
||||
error Text # populated when success=false
|
||||
|
||||
# ── Health ──────────────────────────────────────────────────────────────
|
||||
|
||||
struct Health
|
||||
status Text # "ok" | "no-runtime"
|
||||
backend Text # "ready" | "none"
|
||||
|
||||
# ── Service interface ───────────────────────────────────────────────────
|
||||
|
||||
interface MCP
|
||||
# List tool definitions registered with the runtime.
|
||||
list () -> (list ToolList)
|
||||
|
||||
# Invoke a tool by name with caller-supplied JSON params.
|
||||
call (request CallRequest) -> (result CallResult)
|
||||
|
||||
# Health probe; reports whether the runtime is wired.
|
||||
health () -> (status Health)
|
||||
Reference in New Issue
Block a user