Public DEC-654 still requires a caller bearer; anonymous session now returns that guidance as a tool error instead of silent valid:false. Error text names JWT and API key, Grok bearer_token_file, and local stdio. BearerFromRequest also accepts X-Api-Key / X-Verae-Key. Document Grok stdio vs public URL; add scripts/sync-mcp-bearer.sh.
107 lines
3.9 KiB
Go
107 lines
3.9 KiB
Go
package tools
|
|
|
|
// DEC-654 (D): on the public HTTP server (mcp.verae.ai) every write needs the caller's own credential - a VeraeTime
|
|
// JWT (access_token from login) or a Verae API key - checked at api.veraetime.net - and never falls back to this
|
|
// host's stored service login. Writes: stamp (any scope; global stamps are billed to the caller's account), signup,
|
|
// create_user, session, stamp_batch; login (it would replace the host credential) is refused. Read-only verify and
|
|
// hash_report stay anonymous for the public pages (receipt links, explorer); only those use the host login, and they
|
|
// are never rate limited (DEC-660, George 2026-10-10: "never limit").
|
|
//
|
|
// AI clients (Grok, Claude, Cursor) must send Authorization: Bearer on every tools/call. Grok Build:
|
|
// bearer_token_file = "~/.verae/mcp-bearer" # raw JWT only, not auth.properties
|
|
// or point a local stdio MCP at this binary (Public=false) so the laptop host token is used.
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/veraetime"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// ErrWriteNeedsKey is returned for an anonymous write on the public server.
|
|
var ErrWriteNeedsKey = errors.New(
|
|
"this tool needs your own Verae credential on Authorization: Bearer. " +
|
|
"Use a VeraeTime JWT (access_token from `verae login` / ~/.verae/auth.properties) " +
|
|
"or a Verae API key from https://zap.verae.ai/portal/. " +
|
|
"Grok Build: set mcp_servers.verae-timestamping.bearer_token_file to a file that holds only the raw token " +
|
|
"(e.g. ~/.verae/mcp-bearer), then reconnect the MCP server; " +
|
|
"or run this binary with -transport stdio so the laptop host login is used",
|
|
)
|
|
|
|
// ErrLoginDisabled is returned by the login tool on the public server.
|
|
var ErrLoginDisabled = errors.New(
|
|
"login is not available on the public HTTP server (it would replace the host credential). " +
|
|
"Sign in with `verae login` or at https://zap.verae.ai/portal/, put the JWT or API key in " +
|
|
"Authorization: Bearer on each MCP request (Grok: bearer_token_file), " +
|
|
"or use a local stdio MCP transport",
|
|
)
|
|
|
|
// CredentialChecker validates a caller bearer (implemented by veraetime.Client.CheckCredential).
|
|
type CredentialChecker interface {
|
|
CheckCredential(ctx context.Context, bearer string) error
|
|
}
|
|
|
|
// writeBearer returns the caller's own validated bearer; on the public server there is no host fallback.
|
|
func (d *Deps) writeBearer(ctx context.Context, req *mcp.CallToolRequest) (string, error) {
|
|
if !d.Public {
|
|
return d.resolveBearer(ctx, req)
|
|
}
|
|
tok := BearerFromRequest(req)
|
|
if tok == "" {
|
|
return "", ErrWriteNeedsKey
|
|
}
|
|
if err := d.checkCred(ctx, tok); err != nil {
|
|
return "", err
|
|
}
|
|
return tok, nil
|
|
}
|
|
|
|
// readBearer is the caller's bearer, or (anonymous, never limited - DEC-660) the host login for read-only lookups.
|
|
func (d *Deps) readBearer(ctx context.Context, req *mcp.CallToolRequest) (string, error) {
|
|
return d.resolveBearer(ctx, req)
|
|
}
|
|
|
|
type credEntry struct {
|
|
ok bool
|
|
exp time.Time
|
|
}
|
|
|
|
var (
|
|
credMu sync.Mutex
|
|
credCache = map[[32]byte]credEntry{}
|
|
)
|
|
|
|
func (d *Deps) checkCred(ctx context.Context, tok string) error {
|
|
if d.Checker == nil {
|
|
return ErrWriteNeedsKey // fail closed
|
|
}
|
|
h := sha256.Sum256([]byte(tok))
|
|
credMu.Lock()
|
|
e, hit := credCache[h]
|
|
credMu.Unlock()
|
|
if hit && time.Now().Before(e.exp) {
|
|
if e.ok {
|
|
return nil
|
|
}
|
|
return errors.New("the Verae credential was not accepted")
|
|
}
|
|
err := d.Checker.CheckCredential(ctx, tok)
|
|
var bad *veraetime.CredentialRejected
|
|
if err != nil && !errors.As(err, &bad) {
|
|
return err // upstream unavailable: not cached, fail closed
|
|
}
|
|
credMu.Lock()
|
|
if len(credCache) > 10000 {
|
|
credCache = map[[32]byte]credEntry{}
|
|
}
|
|
credCache[h] = credEntry{ok: err == nil, exp: time.Now().Add(60 * time.Second)}
|
|
credMu.Unlock()
|
|
if err != nil {
|
|
return errors.New("the Verae credential was not accepted")
|
|
}
|
|
return nil
|
|
}
|