Files
zip/caller_mcp_test.go
zeekayandHanzo Dev 5f71d2496b The router is three nouns, and every one of them is concrete
App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:

	users := app.Group("/v1/users")
	users.Post("/", api.Create)

	mint := users.Group("/mint").With(middleware.Mint(gate))
	mint.Post("/deposit", api.Deposit)

A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.

That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.

Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like one.



Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 01:39:17 -07:00

84 lines
2.8 KiB
Go

package zip
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http/httptest"
"testing"
)
// The identity a request carries must reach the handler over EVERY transport,
// because op.invoke is one handler core and the caller is a property of the
// request, not of the door it came through.
//
// MCP was the one door that did not. typed.go and call.go both built the
// handler's context with callerContext(fc); mcp.go used fc.Context(), which
// carries no reference to the *fasthttp.RequestCtx, so CallerOf() came back
// EMPTY for any op dispatched by a tools/call. Two consequences, and the second
// is the expensive one:
//
// - an Authorizer reading the caller could not tell who was asking;
// - an onward zip.Call made from inside such a handler forwarded NO identity
// headers at all, so the next service downstream saw an anonymous request
// that had in fact arrived authenticated.
//
// It is also why hanzoai/cloud grew a hand-installed Bridge() middleware at the
// app root in ~100 places: middleware runs on the fiber Ctx, so parking the
// identity there papered over the hole — in that repo's private vocabulary,
// for that repo only.
func TestCallerReachesTheHandlerOverMCP(t *testing.T) {
type in struct {
X string `json:"x"`
}
type out struct {
Org string `json:"org"`
User string `json:"user"`
}
app := New(Config{AppName: "svc", DisableStartupMessage: true})
app.Get("/v1/who", func(ctx context.Context, _ *in) (*out, error) {
c := CallerOf(ctx)
return &out{Org: c.Org, User: c.User}, nil
}, WithOperationID("who"))
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
req := httptest.NewRequest("POST", "/mcp",
bytes.NewReader([]byte(`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"who","arguments":{}}}`)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(HeaderOrg, "acme")
req.Header.Set(HeaderUser, "u-1")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("POST /mcp: %v", err)
}
defer func() { _ = resp.Body.Close() }()
body, _ := io.ReadAll(resp.Body)
var got struct {
Result struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
} `json:"result"`
}
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("reply is not JSON: %v (%s)", err, body)
}
if len(got.Result.Content) == 0 {
t.Fatalf("tools/call returned no content: %s", body)
}
var who out
if err := json.Unmarshal([]byte(got.Result.Content[0].Text), &who); err != nil {
t.Fatalf("content is not the op's output: %v (%s)", err, got.Result.Content[0].Text)
}
if who.Org != "acme" || who.User != "u-1" {
t.Errorf("CallerOf over MCP = %+v, want org=acme user=u-1 — the tools/call handler "+
"was built with fc.Context() instead of callerContext(fc)\n body: %s", who, body)
}
}