package tools import ( "context" "fmt" "os" "strings" "git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/authctx" "git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/edgebill" "git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/ledger" "git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/veraetime" "github.com/modelcontextprotocol/go-sdk/mcp" ) // Deps are shared backends for MCP tools. type Deps struct { Ledger *ledger.FileLedger Global *veraetime.Client Keeper *veraetime.Keeper RequireBearer bool // Public: the public HTTP server (DEC-654): writes need the caller's own credential; anonymous reads are // never limited (DEC-660). Public bool Checker CredentialChecker // Edge bills global stamps through zap.verae.ai when set (VERAE_MCP_EDGE_URL). Edge *edgebill.Client } func (d *Deps) resolveBearer(ctx context.Context, req *mcp.CallToolRequest) (string, error) { return authctx.Resolve(ctx, BearerFromRequest(req), d.RequireBearer, d.Keeper) } // isCommercialEdgeKey reports tokens that edge apiKeyAuth can map to a prepaid customer. func isCommercialEdgeKey(tok string) bool { t := strings.TrimSpace(tok) return strings.HasPrefix(t, "xVerae-") || strings.HasPrefix(t, "key-") || strings.HasPrefix(t, "zmw_") } func (d *Deps) stamp(ctx context.Context, req *mcp.CallToolRequest, in StampArgs) (*mcp.CallToolResult, ReceiptsOut, error) { if err := assertDigestArgs(req); err != nil { return nil, ReceiptsOut{}, err } kind, err := normalizeReceiptFormat(in.ReceiptFormat) if err != nil { return nil, ReceiptsOut{}, err } scope := strings.TrimSpace(in.Scope) if scope == "" { scope = "local" } if d.Public { if _, err := d.writeBearer(ctx, req); err != nil { return nil, ReceiptsOut{}, err } } var out ReceiptsOut switch scope { case "global": tok, err := d.writeBearer(ctx, req) if err != nil { return nil, ReceiptsOut{}, err } // Single commercial meter: when Edge is configured, bill via services.verae.ai. // Caller must present a commercial key (xVerae- / key- / zmw_), not a bare chain JWT, // so settleStampBilling attributes the customer prepaid ledger. if d.Edge != nil { if !isCommercialEdgeKey(tok) { return nil, ReceiptsOut{}, fmt.Errorf( "commercial stamp requires portal API key (xVerae-… or key-…); get one at https://customer.verae.ai/portal/ — bare VeraeTime JWT is not billed on MCP when VERAE_MCP_EDGE_URL is set", ) } if len(in.Metadata) > 0 || in.Restamp { return nil, ReceiptsOut{}, fmt.Errorf("metadata/restamp on edge-billed stamps is not supported yet; omit metadata or unset VERAE_MCP_EDGE_URL") } er, err := d.Edge.Stamp(ctx, in.SHA256, tok) if err != nil { return nil, ReceiptsOut{}, err } dto := ReceiptDTO{ ReceiptID: er.ReceiptID, Hash: er.Hash, Scope: "global", CreatedAt: er.CreatedAt, Merkle: er.Merkle, CubeID: in.CubeID, OrgID: in.OrgID, } out = ReceiptsOut{Receipts: []ReceiptDTO{dto}} } else { if d.Global == nil { return nil, ReceiptsOut{}, fmt.Errorf("global VeraeTime client not configured") } rec, err := d.Global.StampWithMeta(ctx, in.SHA256, tok, veraetime.StampMeta{ Metadata: in.Metadata, Restamp: in.Restamp, Message: in.Message, }) if err != nil { return nil, ReceiptsOut{}, err } if in.CubeID != "" { rec.CubeID = in.CubeID } if in.OrgID != "" { rec.OrgID = in.OrgID } out = ReceiptsOut{Receipts: []ReceiptDTO{fromGlobal(rec)}} } case "local", "shared": if len(in.Metadata) > 0 || in.Restamp { return nil, ReceiptsOut{}, fmt.Errorf("metadata history is stored on global stamps") } if d.Ledger == nil { return nil, ReceiptsOut{}, fmt.Errorf("ledger not configured") } recs, err := d.Ledger.Stamp(in.SHA256, scope, in.CubeID, in.OrgID) if err != nil { return nil, ReceiptsOut{}, err } out = ReceiptsOut{Receipts: fromLocalList(recs)} default: return nil, ReceiptsOut{}, fmt.Errorf("scope must be local, shared, or global") } out, err = attachArtifacts(out, kind, !d.Public) if err != nil { return nil, ReceiptsOut{}, err } return nil, out, nil } // stampBatch stamps up to 500 digests in one call (DEC-659): global items are charged per new digest to the // caller's own Verae account (own repeats free; the balance is checked for the whole batch first). func (d *Deps) stampBatch(ctx context.Context, req *mcp.CallToolRequest, in StampBatchArgs) (*mcp.CallToolResult, StampBatchOut, error) { if len(in.SHA256s) == 0 || len(in.SHA256s) > 500 { return nil, StampBatchOut{}, fmt.Errorf("sha256s must hold 1..500 digests") } scope := strings.TrimSpace(in.Scope) if scope == "" { scope = "global" } if d.Public { if _, err := d.writeBearer(ctx, req); err != nil { return nil, StampBatchOut{}, err } } switch scope { case "global": tok, err := d.writeBearer(ctx, req) if err != nil { return nil, StampBatchOut{}, err } // Prefer edge meter (per-digest) when configured — same prepaid path as single stamp. if d.Edge != nil { if !isCommercialEdgeKey(tok) { return nil, StampBatchOut{}, fmt.Errorf( "commercial stamp requires portal API key (xVerae-… or key-…); get one at https://customer.verae.ai/portal/", ) } if len(in.Metadata) > 0 || in.Restamp { return nil, StampBatchOut{}, fmt.Errorf("metadata/restamp on edge-billed batch not supported yet") } out := StampBatchOut{Count: len(in.SHA256s)} for i, h := range in.SHA256s { it := BatchItemDTO{Index: i, Hash: h, Status: "submitted"} er, err := d.Edge.Stamp(ctx, h, tok) if err != nil { it.Status, it.Error = "failed", err.Error() out.Failed++ } else { it.ReceiptID = er.ReceiptID it.Status = "completed" out.Created++ } out.Items = append(out.Items, it) } return nil, out, nil } if d.Global == nil { return nil, StampBatchOut{}, fmt.Errorf("global VeraeTime client not configured") } var metas []veraetime.StampMeta if len(in.Metadata) > 0 || in.Restamp { if len(in.Metadata) > 0 && len(in.Metadata) != len(in.SHA256s) { return nil, StampBatchOut{}, fmt.Errorf("metadata must have one entry per digest") } metas = make([]veraetime.StampMeta, len(in.SHA256s)) for i := range in.SHA256s { metas[i].Restamp = in.Restamp metas[i].Message = in.Message if i < len(in.Metadata) { metas[i].Metadata = in.Metadata[i] } } } br, err := d.Global.StampBatchWithMeta(ctx, in.SHA256s, tok, metas) if err != nil { return nil, StampBatchOut{}, err } out := StampBatchOut{Count: br.Count, Created: br.Created, Existing: br.Existing, Failed: br.Failed, TotalCredits: br.TotalCredits, BalanceCredits: br.BalanceCredits} for _, it := range br.Results { out.Items = append(out.Items, BatchItemDTO{Index: it.Index, Hash: it.Digest, JobID: it.JobID, Status: it.Status, Existing: it.Existing, ReceiptID: it.Certificate, Credits: it.Credits, Error: it.Error}) } return nil, out, nil case "local", "shared": if len(in.Metadata) > 0 || in.Restamp { return nil, StampBatchOut{}, fmt.Errorf("metadata history is stored on global stamps") } if d.Ledger == nil { return nil, StampBatchOut{}, fmt.Errorf("ledger not configured") } out := StampBatchOut{Count: len(in.SHA256s)} for i, h := range in.SHA256s { recs, err := d.Ledger.Stamp(h, scope, "", "") it := BatchItemDTO{Index: i, Hash: h, Status: "submitted"} if err != nil { it.Status, it.Error = "failed", err.Error() out.Failed++ } else if len(recs) > 0 { it.ReceiptID = recs[len(recs)-1].ReceiptID out.Created++ } out.Items = append(out.Items, it) } return nil, out, nil default: return nil, StampBatchOut{}, fmt.Errorf("scope must be global, local, or shared") } } func (d *Deps) verify(ctx context.Context, req *mcp.CallToolRequest, in VerifyArgs) (*mcp.CallToolResult, VerifyOut, error) { if err := d.requireAccount(ctx, req); err != nil { return nil, VerifyOut{}, err } if err := assertDigestArgs(req); err != nil { return nil, VerifyOut{}, err } kind, err := normalizeReceiptFormat(in.ReceiptFormat) if err != nil { return nil, VerifyOut{}, err } receiptID := strings.TrimSpace(in.ReceiptID) if body, err := d.resolveVerifyReceiptBody(in); err != nil { return nil, VerifyOut{}, err } else if body != "" { id, err := parseReceiptIDFromBody(kind, body) if err != nil { return nil, VerifyOut{}, err } receiptID = id } if receiptID == "" { return nil, VerifyOut{}, fmt.Errorf("receipt_id is required (or supply receipt / receipt_path)") } want := strings.TrimSpace(in.WantScope) useGlobal := want == "global" || looksLikeCID(receiptID) var ( toolRes *mcp.CallToolResult out VerifyOut ) if useGlobal { toolRes, out, err = d.verifyGlobal(ctx, req, receiptID) } else if d.Ledger != nil { vr := d.Ledger.Verify(receiptID, want) if vr.Valid { out = fromLocalVerify(vr) } else if want == "" && d.Global != nil { // Local miss: try VeraeTime when want_scope is empty (Python RoutingLedger). _, gOut, gErr := d.verifyGlobal(ctx, req, receiptID) if gErr == nil && gOut.Valid { out = gOut } else { out = fromLocalVerify(vr) } } else { out = fromLocalVerify(vr) } } else { toolRes, out, err = d.verifyGlobal(ctx, req, receiptID) } if err != nil { return toolRes, out, err } if out.Valid && out.Receipt != nil { art, aerr := buildArtifact(*out.Receipt, kind, !d.Public) if aerr != nil { return nil, VerifyOut{}, aerr } out.Artifact = &art } return toolRes, out, nil } // resolveVerifyReceiptBody returns inline receipt content from Receipt or ReceiptPath. // Public MCP rejects ReceiptPath (no host-disk reads for callers). func (d *Deps) resolveVerifyReceiptBody(in VerifyArgs) (string, error) { path := strings.TrimSpace(in.ReceiptPath) inline := strings.TrimSpace(in.Receipt) if path != "" { if d.Public { return "", fmt.Errorf("receipt_path is not supported on the public MCP; pass receipt inline or receipt_id") } b, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("receipt_path: %w", err) } return string(b), nil } return inline, nil } func (d *Deps) verifyGlobal(ctx context.Context, req *mcp.CallToolRequest, receiptID string) (*mcp.CallToolResult, VerifyOut, error) { if d.Global == nil { return nil, VerifyOut{Valid: false, Error: "global VeraeTime client not configured"}, nil } tok, err := d.readBearer(ctx, req) if err != nil { return nil, VerifyOut{}, err } vr, err := d.Global.Verify(ctx, receiptID, tok) if err != nil { return nil, VerifyOut{}, err } return nil, fromGlobalVerify(vr), nil } func (d *Deps) hashReport(ctx context.Context, req *mcp.CallToolRequest, in HashReportArgs) (*mcp.CallToolResult, ReceiptsOut, error) { if err := d.requireAccount(ctx, req); err != nil { return nil, ReceiptsOut{}, err } if err := assertDigestArgs(req); err != nil { return nil, ReceiptsOut{}, err } want := strings.TrimSpace(in.WantScope) if want == "global" { return d.hashReportGlobal(ctx, req, in.SHA256) } var local []ReceiptDTO if d.Ledger != nil { recs, err := d.Ledger.HashReport(in.SHA256, in.OrgID, want) if err != nil { return nil, ReceiptsOut{}, err } local = fromLocalList(recs) } if want != "" { return nil, ReceiptsOut{Receipts: local}, nil } // Merge local + global when want_scope is empty. central, err := d.hashReportGlobalList(ctx, req, in.SHA256) if err != nil { return nil, ReceiptsOut{Receipts: local}, nil } seen := make(map[string]struct{}, len(local)) for _, r := range local { seen[r.ReceiptID] = struct{}{} } merged := append([]ReceiptDTO{}, local...) for _, r := range central { if _, ok := seen[r.ReceiptID]; ok { continue } merged = append(merged, r) seen[r.ReceiptID] = struct{}{} } return nil, ReceiptsOut{Receipts: merged}, nil } func (d *Deps) hashReportGlobal(ctx context.Context, req *mcp.CallToolRequest, dig string) (*mcp.CallToolResult, ReceiptsOut, error) { recs, err := d.hashReportGlobalList(ctx, req, dig) if err != nil { return nil, ReceiptsOut{}, err } return nil, ReceiptsOut{Receipts: recs}, nil } func (d *Deps) hashReportGlobalList(ctx context.Context, req *mcp.CallToolRequest, dig string) ([]ReceiptDTO, error) { if d.Global == nil { return nil, fmt.Errorf("global VeraeTime client not configured") } tok, err := d.readBearer(ctx, req) if err != nil { return nil, err } recs, err := d.Global.HashReport(ctx, dig, tok) if err != nil { return nil, err } out := make([]ReceiptDTO, 0, len(recs)) for i := range recs { out = append(out, fromGlobal(&recs[i])) } return out, nil } func (d *Deps) signup(ctx context.Context, _ *mcp.CallToolRequest, in AuthUserPass) (*mcp.CallToolResult, CreateUserOut, error) { if d.Global == nil { return nil, CreateUserOut{}, fmt.Errorf("global VeraeTime client not configured") } if err := requireEmailUsername(in.Username); err != nil { return nil, CreateUserOut{}, err } // Public self-serve: POST /auth/signup needs no caller bearer (DEC-654 write lockdown // does not apply — this creates the credential the caller will use next). // Admin-provisioned users use create_user + admin bearer → POST /auth/users. res, err := d.Global.Signup(ctx, in.Username, in.Password) if err != nil { return nil, CreateUserOut{}, err } lr, token, err := d.Global.Authenticate(ctx, in.Username, in.Password) if err != nil { return nil, CreateUserOut{}, fmt.Errorf("account created but login failed: %w", err) } if !d.Public { if err := d.persistCallerToken(in.Username, in.Password, token, lr.ExpiresAt); err != nil { return nil, CreateUserOut{}, err } } out := CreateUserOut{ ID: res.ID, Username: res.Username, Role: res.Role, AccessToken: token, ExpiresAt: lr.ExpiresAt, HowToUse: tokenHowToUse, } if lr.Username != "" { out.Username = lr.Username } if lr.Role != "" { out.Role = lr.Role } return nil, out, nil } func (d *Deps) login(ctx context.Context, _ *mcp.CallToolRequest, in AuthUserPass) (*mcp.CallToolResult, LoginOut, error) { if d.Global == nil { return nil, LoginOut{}, fmt.Errorf("global VeraeTime client not configured") } // Always authenticate and return access_token so MCP clients (esp. public HTTP) can // set Authorization / bearer_token_file. Persist to host auth.properties only when // not Public — public login must never replace the ns1 service credential. res, token, err := d.Global.Authenticate(ctx, in.Username, in.Password) if err != nil { return nil, LoginOut{}, err } if !d.Public { if err := d.persistCallerToken(in.Username, in.Password, token, res.ExpiresAt); err != nil { return nil, LoginOut{}, err } } return nil, LoginOut{ Username: res.Username, Role: res.Role, ExpiresAt: res.ExpiresAt, AccessToken: token, HowToUse: tokenHowToUse, }, nil } func (d *Deps) persistCallerToken(username, password, token, expiresAt string) error { path := d.Global.AuthPath() if path == "" { path = veraetime.DefaultAuthPath() } return veraetime.WriteProperties(path, map[string]string{ "access_token": token, "id_token": token, "auth_token": token, "expires_at": expiresAt, "username": username, "password": password, }) } func (d *Deps) session(ctx context.Context, req *mcp.CallToolRequest, _ EmptyArgs) (*mcp.CallToolResult, SessionOut, error) { if d.Global == nil { return nil, SessionOut{}, fmt.Errorf("global VeraeTime client not configured") } // Public: never report the host login. Missing caller bearer is an explicit tool error so agents // see how to attach Authorization (silent valid:false hid the fix from Grok/Claude). if d.Public && BearerFromRequest(req) == "" { return nil, SessionOut{Valid: false}, ErrWriteNeedsKey } tok, err := d.resolveBearer(ctx, req) if err != nil { return nil, SessionOut{Valid: false}, nil } res, err := d.Global.Session(ctx, tok) if err != nil { return nil, SessionOut{}, err } return nil, SessionOut{ Valid: res.Valid, Username: res.Username, Role: res.Role, ExpiresAt: res.ExpiresAt, }, nil } func (d *Deps) createUser(ctx context.Context, req *mcp.CallToolRequest, in CreateUserArgs) (*mcp.CallToolResult, CreateUserOut, error) { if d.Global == nil { return nil, CreateUserOut{}, fmt.Errorf("global VeraeTime client not configured") } if err := requireEmailUsername(in.Username); err != nil { return nil, CreateUserOut{}, err } tok, err := d.writeBearer(ctx, req) if err != nil { return nil, CreateUserOut{}, err } res, err := d.Global.CreateUser(ctx, in.Username, in.Password, in.Role, tok) if err != nil { return nil, CreateUserOut{}, err } return nil, CreateUserOut{ID: res.ID, Username: res.Username, Role: res.Role}, nil } func fromLocal(r ledger.Receipt) ReceiptDTO { return ReceiptDTO{ ReceiptID: r.ReceiptID, Hash: r.Hash, Scope: r.Scope, CubeID: r.CubeID, OrgID: r.OrgID, CreatedAt: r.CreatedAt, Merkle: r.Merkle, RestampOf: r.RestampOf, } } func fromLocalList(recs []ledger.Receipt) []ReceiptDTO { out := make([]ReceiptDTO, len(recs)) for i := range recs { out[i] = fromLocal(recs[i]) } return out } func fromLocalVerify(vr ledger.VerifyResult) VerifyOut { out := VerifyOut{Valid: vr.Valid, Error: vr.Error} if vr.Receipt != nil { dto := fromLocal(*vr.Receipt) out.Receipt = &dto } return out } func fromGlobal(r *veraetime.Receipt) ReceiptDTO { if r == nil { return ReceiptDTO{} } return ReceiptDTO{ ReceiptID: r.ReceiptID, Hash: r.Hash, Scope: r.Scope, CubeID: r.CubeID, OrgID: r.OrgID, CreatedAt: r.CreatedAt, Merkle: r.Merkle, RestampOf: r.RestampOf, } } func fromGlobalVerify(vr veraetime.VerifyResult) VerifyOut { out := VerifyOut{Valid: vr.Valid, Error: vr.Error} if vr.Receipt != nil { dto := fromGlobal(vr.Receipt) out.Receipt = &dto } return out }