feat(mcp): oauth_start / oauth_complete for social login
Browser OAuth via customer.verae.ai portal (authentik at auth.verae.ai). oauth_start returns authorize_url+state; oauth_complete polls until xVerae- access_token. Open without bearer on public MCP; welcome/register copy updated.
This commit is contained in:
parent
3ec1f0516b
commit
2eb9ec72c6
6 changed files with 349 additions and 19 deletions
|
|
@ -55,7 +55,7 @@ func TestStreamableHTTP_InitializeListStampLocal(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("tools/list: %v", err)
|
t.Fatalf("tools/list: %v", err)
|
||||||
}
|
}
|
||||||
wantTools := []string{"welcome", "stamp", "verify", "hash_report", "signup", "login", "session", "create_user"}
|
wantTools := []string{"welcome", "stamp", "verify", "hash_report", "signup", "login", "oauth_start", "oauth_complete", "session", "create_user"}
|
||||||
got := map[string]bool{}
|
got := map[string]bool{}
|
||||||
for _, tool := range tools.Tools {
|
for _, tool := range tools.Tools {
|
||||||
got[tool.Name] = true
|
got[tool.Name] = true
|
||||||
|
|
|
||||||
243
internal/tools/oauth.go
Normal file
243
internal/tools/oauth.go
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default portal base for OAuth (authentik-brokered via edge).
|
||||||
|
const defaultOauthPortalURL = "https://customer.verae.ai"
|
||||||
|
|
||||||
|
// OauthStartArgs begins a browser OAuth window flow.
|
||||||
|
type OauthStartArgs struct {
|
||||||
|
Provider string `json:"provider,omitempty" jsonschema:"google|github|apple|x (default google); authentik Source slug"`
|
||||||
|
AcceptTos bool `json:"accept_tos,omitempty" jsonschema:"required true when creating a new account via social login"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OauthStartOut is returned by oauth_start.
|
||||||
|
type OauthStartOut struct {
|
||||||
|
AuthorizeURL string `json:"authorize_url"`
|
||||||
|
State string `json:"state"`
|
||||||
|
ExpiresAt string `json:"expires_at,omitempty"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
HowToUse string `json:"how_to_use"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OauthCompleteArgs finishes OAuth after the browser callback (poll edge result).
|
||||||
|
type OauthCompleteArgs struct {
|
||||||
|
State string `json:"state" jsonschema:"state from oauth_start"`
|
||||||
|
WaitSec int `json:"wait_sec,omitempty" jsonschema:"seconds to poll (default 45, max 120)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OauthCompleteOut is returned when social login succeeds.
|
||||||
|
type OauthCompleteOut struct {
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
CustomerID string `json:"customer_id,omitempty"`
|
||||||
|
Created bool `json:"created,omitempty"`
|
||||||
|
AccessToken string `json:"access_token,omitempty"`
|
||||||
|
HowToUse string `json:"how_to_use,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"` // pending when still waiting (short poll)
|
||||||
|
}
|
||||||
|
|
||||||
|
func oauthPortalBase() string {
|
||||||
|
u := strings.TrimRight(strings.TrimSpace(os.Getenv("VERAE_OAUTH_PORTAL_URL")), "/")
|
||||||
|
if u == "" {
|
||||||
|
u = defaultOauthPortalURL
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deps) oauthHTTPClient() *http.Client {
|
||||||
|
return &http.Client{Timeout: 30 * time.Second}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deps) oauthStart(ctx context.Context, _ *mcp.CallToolRequest, in OauthStartArgs) (*mcp.CallToolResult, OauthStartOut, error) {
|
||||||
|
provider := strings.ToLower(strings.TrimSpace(in.Provider))
|
||||||
|
if provider == "" {
|
||||||
|
provider = "google"
|
||||||
|
}
|
||||||
|
switch provider {
|
||||||
|
case "google", "github", "apple", "x":
|
||||||
|
default:
|
||||||
|
return nil, OauthStartOut{}, fmt.Errorf("provider must be google, github, apple, or x")
|
||||||
|
}
|
||||||
|
q := url.Values{
|
||||||
|
"provider": {provider},
|
||||||
|
"popup": {"1"},
|
||||||
|
}
|
||||||
|
if in.AcceptTos {
|
||||||
|
q.Set("accept_tos", "1")
|
||||||
|
}
|
||||||
|
endpoint := oauthPortalBase() + "/portal/api/oauth/start?" + q.Encode()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, OauthStartOut{}, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
res, err := d.oauthHTTPClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, OauthStartOut{}, fmt.Errorf("oauth start: %w", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||||
|
if res.StatusCode == http.StatusServiceUnavailable {
|
||||||
|
return nil, OauthStartOut{}, fmt.Errorf("oauth not configured on portal (%s)", strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return nil, OauthStartOut{}, fmt.Errorf("oauth start HTTP %d: %s", res.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
var raw struct {
|
||||||
|
AuthorizeURL string `json:"authorize_url"`
|
||||||
|
State string `json:"state"`
|
||||||
|
ExpiresAt string `json:"expires_at"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
|
return nil, OauthStartOut{}, fmt.Errorf("oauth start parse: %w", err)
|
||||||
|
}
|
||||||
|
if raw.AuthorizeURL == "" || raw.State == "" {
|
||||||
|
msg := raw.Error
|
||||||
|
if raw.Detail != "" {
|
||||||
|
msg = raw.Detail
|
||||||
|
}
|
||||||
|
if msg == "" {
|
||||||
|
msg = "missing authorize_url/state"
|
||||||
|
}
|
||||||
|
return nil, OauthStartOut{}, fmt.Errorf("oauth start: %s", msg)
|
||||||
|
}
|
||||||
|
how := "Open authorize_url in a browser window (or system browser). " +
|
||||||
|
"Complete sign-in at auth.verae.ai. Then call tool oauth_complete with the same state. " +
|
||||||
|
"For a new account, call oauth_start again with accept_tos=true after accepting Terms on the portal if needed."
|
||||||
|
return nil, OauthStartOut{
|
||||||
|
AuthorizeURL: raw.AuthorizeURL,
|
||||||
|
State: raw.State,
|
||||||
|
ExpiresAt: raw.ExpiresAt,
|
||||||
|
Provider: provider,
|
||||||
|
HowToUse: how,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deps) oauthComplete(ctx context.Context, _ *mcp.CallToolRequest, in OauthCompleteArgs) (*mcp.CallToolResult, OauthCompleteOut, error) {
|
||||||
|
state := strings.TrimSpace(in.State)
|
||||||
|
if state == "" {
|
||||||
|
return nil, OauthCompleteOut{}, fmt.Errorf("state is required (from oauth_start)")
|
||||||
|
}
|
||||||
|
wait := in.WaitSec
|
||||||
|
if wait <= 0 {
|
||||||
|
wait = 45
|
||||||
|
}
|
||||||
|
if wait > 120 {
|
||||||
|
wait = 120
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(time.Duration(wait) * time.Second)
|
||||||
|
var lastPending bool
|
||||||
|
for {
|
||||||
|
out, pending, err := d.pollOauthResult(ctx, state)
|
||||||
|
if err != nil {
|
||||||
|
return nil, OauthCompleteOut{}, err
|
||||||
|
}
|
||||||
|
if !pending {
|
||||||
|
if !d.Public && out.AccessToken != "" {
|
||||||
|
_ = d.persistOauthBearer(out.Username, out.AccessToken)
|
||||||
|
}
|
||||||
|
out.HowToUse = tokenHowToUse
|
||||||
|
return nil, out, nil
|
||||||
|
}
|
||||||
|
lastPending = true
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, OauthCompleteOut{}, ctx.Err()
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastPending {
|
||||||
|
return nil, OauthCompleteOut{
|
||||||
|
Status: "pending",
|
||||||
|
HowToUse: "Browser sign-in not finished yet. Open authorize_url from oauth_start, then call oauth_complete again with the same state.",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, OauthCompleteOut{}, fmt.Errorf("oauth complete: unexpected empty result")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deps) pollOauthResult(ctx context.Context, state string) (OauthCompleteOut, bool, error) {
|
||||||
|
endpoint := oauthPortalBase() + "/portal/api/oauth/result?state=" + url.QueryEscape(state)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return OauthCompleteOut{}, false, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
res, err := d.oauthHTTPClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return OauthCompleteOut{}, false, fmt.Errorf("oauth result: %w", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||||
|
switch res.StatusCode {
|
||||||
|
case http.StatusAccepted:
|
||||||
|
return OauthCompleteOut{Status: "pending"}, true, nil
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return OauthCompleteOut{}, false, fmt.Errorf("unknown_or_expired_state")
|
||||||
|
}
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return OauthCompleteOut{}, false, fmt.Errorf("oauth result HTTP %d: %s", res.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
var raw struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
APIKey string `json:"apiKey"`
|
||||||
|
Created bool `json:"created"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
CustomerID string `json:"customerId"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
|
return OauthCompleteOut{}, false, fmt.Errorf("oauth result parse: %w", err)
|
||||||
|
}
|
||||||
|
if raw.Status == "pending" {
|
||||||
|
return OauthCompleteOut{Status: "pending"}, true, nil
|
||||||
|
}
|
||||||
|
if !raw.OK {
|
||||||
|
msg := raw.Error
|
||||||
|
if raw.Detail != "" {
|
||||||
|
msg = raw.Detail
|
||||||
|
}
|
||||||
|
if msg == "" {
|
||||||
|
msg = "oauth failed"
|
||||||
|
}
|
||||||
|
return OauthCompleteOut{}, false, fmt.Errorf("%s", msg)
|
||||||
|
}
|
||||||
|
// Prefer long-lived xVerae- portal key for commercial MCP stamps when EDGE_URL is set.
|
||||||
|
tok := strings.TrimSpace(raw.APIKey)
|
||||||
|
if tok == "" {
|
||||||
|
tok = strings.TrimSpace(raw.Token)
|
||||||
|
}
|
||||||
|
return OauthCompleteOut{
|
||||||
|
Username: raw.Email,
|
||||||
|
CustomerID: raw.CustomerID,
|
||||||
|
Created: raw.Created,
|
||||||
|
AccessToken: tok,
|
||||||
|
}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deps) persistOauthBearer(username, token string) error {
|
||||||
|
if d.Global == nil || token == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.persistCallerToken(username, "", token, "")
|
||||||
|
}
|
||||||
71
internal/tools/oauth_test.go
Normal file
71
internal/tools/oauth_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOauthStartAndComplete(t *testing.T) {
|
||||||
|
var startHits, resultHits int
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/portal/api/oauth/start":
|
||||||
|
startHits++
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"authorize_url": "https://auth.verae.ai/application/o/authorize/?x=1",
|
||||||
|
"state": "st-test",
|
||||||
|
"expires_at": "2099-01-01T00:00:00Z",
|
||||||
|
"provider": "google",
|
||||||
|
})
|
||||||
|
case r.URL.Path == "/portal/api/oauth/result":
|
||||||
|
resultHits++
|
||||||
|
if resultHits < 2 {
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"status": "pending"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"ok": true,
|
||||||
|
"token": "portal-session",
|
||||||
|
"apiKey": "xVerae-test-key",
|
||||||
|
"created": true,
|
||||||
|
"email": "user@example.com",
|
||||||
|
"customerId": "cust_abc",
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
t.Setenv("VERAE_OAUTH_PORTAL_URL", srv.URL)
|
||||||
|
|
||||||
|
d := &Deps{Public: true}
|
||||||
|
_, start, err := d.oauthStart(context.Background(), nil, OauthStartArgs{Provider: "google", AcceptTos: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if start.State != "st-test" || start.AuthorizeURL == "" {
|
||||||
|
t.Fatalf("%+v", start)
|
||||||
|
}
|
||||||
|
_, done, err := d.oauthComplete(context.Background(), nil, OauthCompleteArgs{State: start.State, WaitSec: 5})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if done.AccessToken != "xVerae-test-key" || done.Username != "user@example.com" || !done.Created {
|
||||||
|
t.Fatalf("%+v", done)
|
||||||
|
}
|
||||||
|
if startHits != 1 || resultHits < 2 {
|
||||||
|
t.Fatalf("hits start=%d result=%d", startHits, resultHits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOauthStartRejectsBadProvider(t *testing.T) {
|
||||||
|
d := &Deps{Public: true}
|
||||||
|
_, _, err := d.oauthStart(context.Background(), nil, OauthStartArgs{Provider: "facebook"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
package tools
|
package tools
|
||||||
|
|
||||||
// DEC-654 (D): on the public HTTP server (mcp.verae.ai) account and auth are required for use. Every tool except
|
// DEC-654 (D): on the public HTTP server (mcp.verae.ai) account and auth are required for use. Every tool except
|
||||||
// welcome, signup, and login needs the caller's own credential — a VeraeTime JWT (access_token from signup/login)
|
// welcome, signup, login, oauth_start, and oauth_complete needs the caller's own credential — a VeraeTime JWT
|
||||||
// or a Verae API key — checked at api.veraetime.net — and never falls back to this host's stored service login.
|
// (access_token from signup/login/oauth) or a Verae API key — checked at api.veraetime.net — and never falls back
|
||||||
|
// to this host's stored service login.
|
||||||
// Tools that need a caller bearer: stamp, stamp_batch, verify, hash_report, session, create_user.
|
// Tools that need a caller bearer: stamp, stamp_batch, verify, hash_report, session, create_user.
|
||||||
// Public signup and login are open (no bearer): they create/authenticate the account and return access_token so the
|
// Public signup, login, and oauth_* are open (no bearer): they create/authenticate the account and return
|
||||||
// client can set Authorization / bearer_token_file. They never write the host auth.properties on the public server.
|
// access_token so the client can set Authorization / bearer_token_file. They never write the host auth.properties
|
||||||
// welcome is open so new users can read onboarding before signing up.
|
// on the public server. welcome is open so new users can read onboarding before signing up.
|
||||||
//
|
//
|
||||||
// AI clients (Grok, Claude, Cursor) must send Authorization: Bearer on every tools/call after signup/login. Grok Build:
|
// AI clients (Grok, Claude, Cursor) must send Authorization: Bearer on every tools/call after signup/login. Grok Build:
|
||||||
// bearer_token_file = "~/.verae/mcp-bearer" # raw JWT only, not auth.properties
|
// bearer_token_file = "~/.verae/mcp-bearer" # raw JWT only, not auth.properties
|
||||||
|
|
@ -26,9 +27,9 @@ import (
|
||||||
// ErrWriteNeedsKey is returned when the public server is used without a caller account/credential.
|
// ErrWriteNeedsKey is returned when the public server is used without a caller account/credential.
|
||||||
var ErrWriteNeedsKey = errors.New(
|
var ErrWriteNeedsKey = errors.New(
|
||||||
"account and auth are required for use. " +
|
"account and auth are required for use. " +
|
||||||
"Call tool welcome for onboarding, then signup (new account) or login (existing) to receive a VeraeTime JWT as access_token. " +
|
"Call tool welcome for onboarding, then signup/login or oauth_start→oauth_complete to receive a VeraeTime JWT or xVerae- key as access_token. " +
|
||||||
"Write that token to your client's bearer file (Grok: ~/.verae/mcp-bearer via bearer_token_file) " +
|
"Write that token to your client's bearer file (Grok: ~/.verae/mcp-bearer via bearer_token_file) " +
|
||||||
"or use a Verae API key from https://zap.verae.ai/portal/, " +
|
"or use a Verae API key from https://customer.verae.ai/portal/, " +
|
||||||
"then send Authorization: Bearer on each tools/call. " +
|
"then send Authorization: Bearer on each tools/call. " +
|
||||||
"Or run this binary with -transport stdio so the laptop host login is used",
|
"Or run this binary with -transport stdio so the laptop host login is used",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,17 @@ func Register(s *mcp.Server, d *Deps) {
|
||||||
"On the public HTTP server the host credential is never replaced — only the tool result carries the token. " +
|
"On the public HTTP server the host credential is never replaced — only the tool result carries the token. " +
|
||||||
"On stdio/local transport the token is also stored on this host (auth.properties).",
|
"On stdio/local transport the token is also stored on this host (auth.properties).",
|
||||||
}, d.login)
|
}, d.login)
|
||||||
|
mcp.AddTool(s, &mcp.Tool{
|
||||||
|
Name: "oauth_start",
|
||||||
|
Description: "Start social login (Google/GitHub/Apple/X) via auth.verae.ai. Returns authorize_url + state. " +
|
||||||
|
"Open authorize_url in a browser window, then call oauth_complete with the same state. " +
|
||||||
|
"For a new account set accept_tos=true. No Authorization required.",
|
||||||
|
}, d.oauthStart)
|
||||||
|
mcp.AddTool(s, &mcp.Tool{
|
||||||
|
Name: "oauth_complete",
|
||||||
|
Description: "Finish social login after the browser callback. Pass state from oauth_start; polls until done or wait_sec. " +
|
||||||
|
"Returns access_token (prefer xVerae- portal key for commercial stamps). No Authorization required.",
|
||||||
|
}, d.oauthComplete)
|
||||||
mcp.AddTool(s, &mcp.Tool{
|
mcp.AddTool(s, &mcp.Tool{
|
||||||
Name: "session",
|
Name: "session",
|
||||||
Description: "Validate the caller's VeraeTime JWT (Authorization: Bearer). Returns valid, username, role, and expires_at.",
|
Description: "Validate the caller's VeraeTime JWT (Authorization: Bearer). Returns valid, username, role, and expires_at.",
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,21 @@ Verae Timestamping proves that a specific digital object existed at a point in t
|
||||||
|
|
||||||
This MCP server is how AI clients (Grok, Claude, Cursor, and others) and local tools talk to Verae:
|
This MCP server is how AI clients (Grok, Claude, Cursor, and others) and local tools talk to Verae:
|
||||||
• welcome — onboarding (no account required)
|
• welcome — onboarding (no account required)
|
||||||
• signup / login — create an account or sign in; you receive an access_token (required next)
|
• signup / login — create an account or sign in with email+password; you receive an access_token (required next)
|
||||||
|
• oauth_start / oauth_complete — social login (Google/GitHub/Apple/X) via a browser window at auth.verae.ai
|
||||||
• stamp / stamp_batch — commit one or many SHA-256 digests (local ledger or global VeraeTime)
|
• stamp / stamp_batch — commit one or many SHA-256 digests (local ledger or global VeraeTime)
|
||||||
• verify / hash_report — check a receipt or list history for a digest
|
• verify / hash_report — check a receipt or list history for a digest
|
||||||
• session — confirm your bearer is still valid
|
• session — confirm your bearer is still valid
|
||||||
|
|
||||||
Public HTTP (mcp.verae.ai): every tool except welcome, signup, and login needs Authorization: Bearer (VeraeTime JWT or xVerae- API key). There is no host-login fallback. Put access_token in your client's bearer file (Grok: bearer_token_file → ~/.verae/mcp-bearer) or send Authorization on each tools/call. Do not paste tokens into chat.
|
Public HTTP (mcp.verae.ai): every tool except welcome, signup, login, oauth_start, and oauth_complete needs Authorization: Bearer (VeraeTime JWT or xVerae- API key). There is no host-login fallback. Put access_token in your client's bearer file (Grok: bearer_token_file → ~/.verae/mcp-bearer) or send Authorization on each tools/call. Do not paste tokens into chat.
|
||||||
|
|
||||||
Important: the username must be an email address (welcome mail is sent there on signup).
|
Important: the username must be an email address (welcome mail is sent there on signup).
|
||||||
|
|
||||||
Next step — create or sign in to your account:
|
Next step — create or sign in to your account:
|
||||||
1. New here? Call tool signup with your email as username and a password. It creates the account, emails a Welcome to Verae message, and returns access_token.
|
1. Social: call oauth_start (accept_tos=true for new accounts), open authorize_url in a browser, then oauth_complete with state → access_token.
|
||||||
2. Already registered? Call tool login with your email and password to receive access_token.
|
2. New here with password? Call tool signup with your email as username and a password → access_token (Welcome email is sent).
|
||||||
3. Save the token in your MCP client, call session to confirm, then stamp or verify.
|
3. Already registered? Call tool login with your email and password → access_token.
|
||||||
|
4. Save the token in your MCP client, call session to confirm, then stamp or verify.
|
||||||
|
|
||||||
Global stamps may meter prepaid credits when edge billing is enabled.`
|
Global stamps may meter prepaid credits when edge billing is enabled.`
|
||||||
|
|
||||||
|
|
@ -40,8 +42,9 @@ const instructions = "Welcome to Verae Timestamping MCP (mcp.verae.ai). " +
|
||||||
"tell them the username must be an email address (a Welcome to Verae email is sent on signup), " +
|
"tell them the username must be an email address (a Welcome to Verae email is sent on signup), " +
|
||||||
"then ask them to sign in (tool login) or create an account (tool signup). " +
|
"then ask them to sign in (tool login) or create an account (tool signup). " +
|
||||||
"Prefer calling tool welcome to show the full onboarding text, then guide signup or login. " +
|
"Prefer calling tool welcome to show the full onboarding text, then guide signup or login. " +
|
||||||
"Only welcome, signup, and login work without a bearer. " +
|
"Only welcome, signup, login, oauth_start, and oauth_complete work without a bearer. " +
|
||||||
"After signup/login, save access_token to the client's bearer file (Grok: ~/.verae/mcp-bearer via bearer_token_file) or Authorization header — never paste tokens into chat. " +
|
"Social login: oauth_start → open authorize_url → oauth_complete. " +
|
||||||
|
"After signup/login/oauth, save access_token to the client's bearer file (Grok: ~/.verae/mcp-bearer via bearer_token_file) or Authorization header — never paste tokens into chat. " +
|
||||||
"When VERAE_MCP_EDGE_URL is set, global stamps meter on zap.verae.ai. " +
|
"When VERAE_MCP_EDGE_URL is set, global stamps meter on zap.verae.ai. " +
|
||||||
"Admin create_user is for operators only."
|
"Admin create_user is for operators only."
|
||||||
|
|
||||||
|
|
@ -59,12 +62,13 @@ func (d *Deps) welcome(_ context.Context, _ *mcp.CallToolRequest, _ EmptyArgs) (
|
||||||
Title: "Welcome to Verae",
|
Title: "Welcome to Verae",
|
||||||
Message: welcomeMessage,
|
Message: welcomeMessage,
|
||||||
NextSteps: []string{
|
NextSteps: []string{
|
||||||
"New user: call tool signup with your email as username and a password → receive access_token (Welcome email is sent)",
|
"Social: oauth_start (accept_tos=true if new) → open authorize_url → oauth_complete → access_token",
|
||||||
"Existing user: call tool login with your email and password → receive access_token",
|
"New user (password): signup with email username + password → access_token (Welcome email is sent)",
|
||||||
|
"Existing user (password): login with email + password → access_token",
|
||||||
"Save access_token to your MCP bearer file or Authorization header (do not paste into chat)",
|
"Save access_token to your MCP bearer file or Authorization header (do not paste into chat)",
|
||||||
"Call tool session to confirm, then stamp a SHA-256 digest",
|
"Call tool session to confirm, then stamp a SHA-256 digest",
|
||||||
},
|
},
|
||||||
ToolsHint: []string{"welcome", "signup", "login", "session", "stamp", "stamp_batch", "verify", "hash_report"},
|
ToolsHint: []string{"welcome", "oauth_start", "oauth_complete", "signup", "login", "session", "stamp", "stamp_batch", "verify", "hash_report"},
|
||||||
HasAccount: "Account and auth are required for use. The username must be an email address. Do you already have a Verae account? Sign in with login, or create one with signup.",
|
HasAccount: "Account and auth are required for use. Prefer oauth_start/oauth_complete for Google/GitHub/Apple/X, or signup/login with an email username.",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue