frege

package module
v0.0.0-...-60d5313 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 13 Imported by: 0

README

frege-go

A small, dependency-free Go client for the Frege HTTP API.

Frege turns each project's OpenAPI spec into a set of tools and runs them against the upstream API with the right credential injected. This client lets your own code call those tools directly — a Telegram bot, a cron job, a backend service — with no AI model in the loop.

res, err := client.InvokeTool(ctx, projectID, "get_account_profile", nil)
fmt.Println(res.StatusCode, res.Body) // the upstream's own response

The whole package is one file and the standard library. Nothing else.

Install

go get github.com/MultiAI-Labs/frege-go

Quickest start: an API key

Mint a key in the dashboard: Project → API keys. It is shown once, starts with frege_sk_, and belongs to that one project. There is no sign-in and no token to refresh — a key is a static credential your code holds:

package main

import (
	"context"
	"fmt"
	"os"

	frege "github.com/MultiAI-Labs/frege-go"
)

func main() {
	client := frege.New(frege.StaticToken(os.Getenv("FREGE_API_KEY")))

	res, err := client.InvokeTool(context.Background(), 3, "get_account_profile", nil)
	if err != nil {
		panic(err)
	}
	fmt.Println("upstream said", res.StatusCode)
	fmt.Println(res.Body)
}

That is the whole setup. A key can list a project's tools and invoke them, bounded by the project's write policy, and nothing else — it cannot reach project settings or any other project. Keep it secret like any password; anyone holding it can run that project's tools. Revoke it in the dashboard if it leaks.

InvokeTool returns the upstream's own response. res.StatusCode and res.Body are what the third-party API answered. A 404 there means the upstream returned 404 — not that the tool is missing. A missing tool, a bad argument, or an auth failure comes back as an *APIError instead (see below).

Arguments are keyed by the tool's input schema:

res, err := client.InvokeTool(ctx, 3, "add_favorite", map[string]any{
	"stock_id": 42,
})

Discover a project's tools

You need the numeric project id (it is in the dashboard URL). Then:

ops, err := client.ListOperations(ctx, 3)
for _, op := range ops {
	fmt.Printf("%s  %s %s — %s\n", op.ToolName, op.Method, op.Path, op.Summary)
	// op.InputSchema is the JSON Schema for its arguments.
}

Errors

Failures from Frege itself are a typed *APIError:

res, err := client.InvokeTool(ctx, 3, "add_favorite", args)
if err != nil {
	var apiErr *frege.APIError
	if errors.As(err, &apiErr) {
		fmt.Println(apiErr.StatusCode, apiErr.Code, apiErr.Message)
		fmt.Println("request id:", apiErr.RequestID) // quote this to support
		if apiErr.StatusCode == 422 {
			fmt.Println("bad arguments:", apiErr.Fields)
		}
	}
}

Environments

Environment Base URL
Production https://frege.io (default)
Dev https://frege.uz
client := frege.New(frege.StaticToken(key), frege.WithBaseURL("https://frege.uz"))

Alternative: act as a user (no API key)

If you have no API key — or you specifically want to act as a person rather than a project credential — sign in once with an email code and let the client keep the session alive. This is heavier: it needs a one-time human step and a rotating refresh token.

Get a refresh token once:

go run ./examples/bootstrap
# Email: you@yourcompany.com  → (check inbox) → Code: 123456
# prints a refresh token to store

Then use it:

tok := frege.NewRefreshingToken("", os.Getenv("FREGE_REFRESH_TOKEN"),
	frege.WithOnRefresh(func(_, refresh string) {
		_ = os.WriteFile("frege_refresh_token", []byte(refresh), 0o600) // it rotates
	}),
)
client := frege.New(tok)

A user token also reaches endpoints an API key cannot, such as client.ListProjects(ctx, orgID).

When you want an agent, not a script

This SDK is the deterministic path: your code decides which tool to run. If you instead want an AI agent to pick and chain tools, Frege's project is already a standard MCP server. Point the official modelcontextprotocol/go-sdk client at your project's clean URL and it just works:

https://frege.io/mcp/{org-slug}/{project-slug}

That path uses a different token (an MCP-audience OAuth token, not the one this SDK uses). See the Frege docs for the MCP connect flow.

Reference

Method What it does API key?
client.InvokeTool(ctx, projectID, name, args, opts…) Run one tool ✅
client.ListOperations(ctx, projectID) The project's tools + input schemas ✅
client.ListProjects(ctx, orgID) Projects in one organization user only
frege.SendMagicCode(ctx, baseURL, email) Email a sign-in code —
frege.VerifyMagicCode(ctx, baseURL, email, code) Exchange the code for a Session —
frege.NewRefreshingToken(access, refresh, opts…) A user-token source that auto-refreshes —

When each customer has their own upstream credential, pass frege.AsClient(customerID) to InvokeTool. All calls take a context.Context, so timeouts and cancellation work as usual.

Documentation

Overview

Package frege is a small, dependency-free Go client for the Frege HTTP API.

Frege turns each project's OpenAPI spec into a set of tools and runs them against the upstream API with the right credential injected. This client lets your own code — a Telegram bot, a cron job, a backend service — call those tools directly, with no AI model in the loop.

Auth in one paragraph

Use a project API key. Mint one in the dashboard under Project → API keys; it is shown once and starts with frege_sk_. It is scoped to that one project, it does not expire, and it needs no refresh machinery:

c := frege.New(frege.StaticToken("frege_sk_…"))
res, err := c.InvokeTool(ctx, projectID, "get_account_profile", nil)

A Frege USER's access token works too, for code acting as a person rather than as a service. Those are short-lived, so sign in once with an email code (see the bootstrap example) and let a *RefreshingToken trade the refresh token for new access tokens. Refresh tokens ROTATE — every refresh invalidates the old one — so persist each new pair via OnRefresh or a restart will present a token the server no longer accepts.

tok := frege.NewRefreshingToken("", storedRefreshToken,
    frege.WithOnRefresh(func(access, refresh string) { save(refresh) }))
c := frege.New(tok)

Acting as one customer

On a project whose end customers each sign in with their own upstream account, AsClient names WHICH customer's credential to spend. One key, one agent, any customer — and both parties land in the audit trail.

res, err := c.InvokeTool(ctx, projectID, "get_account_profile", nil,
    frege.AsClient(customerID))

Index

Constants

View Source
const (
	StageStaging = "staging"
	StageLive    = "live"
)

Client talks to one Frege environment as one user. The two environments every project has. A project-scoped call is answered by one of them, and the choice is carried in a header.

View Source
const DefaultBaseURL = "https://frege.io"

DefaultBaseURL is Frege's production API. Use https://frege.uz for dev.

View Source
const StageHeader = "X-Frege-Stage"

StageHeader carries the environment on every project-scoped request.

Variables

This section is empty.

Functions

func SendMagicCode

func SendMagicCode(ctx context.Context, baseURL, email string) error

SendMagicCode emails a six-digit sign-in code to the address, creating the account on first use. Follow with VerifyMagicCode. baseURL may be "" for DefaultBaseURL.

Types

type APIError

type APIError struct {
	StatusCode int
	Code       string
	Message    string
	Fields     map[string]string
	RequestID  string
}

APIError is a non-2xx response from Frege itself (not from the upstream API).

func (*APIError) Error

func (e *APIError) Error() string

Error implements error.

func (*APIError) IsAuth

func (e *APIError) IsAuth() bool

IsAuth reports whether the error is an authentication failure (401).

type Client

type Client struct {
	// contains filtered or unexported fields
}

func New

func New(tokens TokenSource, opts ...Option) *Client

New builds a Client. tokens supplies the bearer for each call — usually a *RefreshingToken built from a stored refresh token.

func (*Client) InvokeTool

func (c *Client) InvokeTool(ctx context.Context, projectID int64, toolName string, args map[string]any, opts ...InvokeOption) (*ToolResult, error)

InvokeTool runs one tool and returns the raw upstream response. args are keyed by the tool's input schema; pass nil for a tool that takes none.

The returned ToolResult.StatusCode and Body are the UPSTREAM's own: a 404 there means the upstream answered 404, not that the tool is missing. A missing tool, a bad argument, or an auth problem is returned as an *APIError instead.

func (*Client) ListOperations

func (c *Client) ListOperations(ctx context.Context, projectID int64) ([]Operation, error)

ListOperations returns the tools generated from the project's active spec: their names, HTTP method/path, summary, and JSON input schema.

func (*Client) ListProjects

func (c *Client) ListProjects(ctx context.Context, orgID int64) ([]Project, error)

ListProjects returns the projects the signed-in user can reach in one organization. org_id is required by the API; find it in the dashboard URL.

type InvokeOption

type InvokeOption func(*invokeBody)

InvokeOption tunes a single InvokeTool call.

func AsClient

func AsClient(clientID int64) InvokeOption

AsClient runs the tool with one connected customer's stored credential instead of the project's own. Required wherever the project has no shared project-level credential.

The id comes back from enrolling the customer. If you did not keep it, use AsCustomer instead — it is almost always the better call.

func AsCustomer

func AsCustomer(reference string) InvokeOption

AsCustomer runs the tool as the customer you enrolled under this reference.

The reference is the external_ref you supplied when connecting them: the phone number, the chat id, whatever your channel already knows them by. That is the point — a bot holds a chat id, not a row id, and keeping a table that maps one to the other means running a database whose only job is to remember something Frege already knows.

It is a LABEL and not a secret. It authorises nothing on its own: your API key is what says you may spend this project's customers' credentials, and it could spend this one by id regardless. What the customer's own sign-in proved, it proved at enrolment.

One caveat worth knowing before you rely on it. The same person enrolled twice — once in a browser, once through your bot — is two customers holding two different upstream credentials, because linking them needs a verified claim common to both and Frege will not guess. A reference that names both is REFUSED rather than resolved, since picking one would spend an account you did not name. Use AsClient with the id when that happens.

type Operation

type Operation struct {
	ID          string         `json:"id"`
	ToolName    string         `json:"tool_name"`
	Method      string         `json:"method"`
	Path        string         `json:"path"`
	Summary     string         `json:"summary"`
	Description string         `json:"description"`
	Tags        []string       `json:"tags"`
	InputSchema map[string]any `json:"input_schema"`
}

Operation is one tool generated from the project's spec.

type Option

type Option func(*Client)

Option configures a Client.

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL points the client at a specific environment (default DefaultBaseURL).

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies your own *http.Client (timeouts, proxy, and so on).

func WithStage

func WithStage(stage string) Option

WithStage picks the environment every project-scoped call is answered by: frege.StageStaging or frege.StageLive.

OMITTING THIS MEANS LIVE. That is the server's default, not this client's choice, and it is the one thing worth knowing before pointing a test at a real deployment: a run that means to exercise staging and never sets a stage reads production's spec and spends production's credential, and every response looks perfectly normal.

An API key is bound to ONE environment when it is issued, so a key and a stage that disagree fail rather than crossing over. That is the server protecting you, not this option.

type Project

type Project struct {
	ID              int64  `json:"id"`
	OrgID           int64  `json:"org_id"`
	Role            string `json:"role"`
	Slug            string `json:"slug"`
	Name            string `json:"name"`
	Description     string `json:"description"`
	UpstreamBaseURL string `json:"upstream_base_url"`
	MCPURL          string `json:"mcp_url"`
	WritePolicy     string `json:"write_policy"`
}

Project is one Frege project the user can reach.

type RTOption

type RTOption func(*RefreshingToken)

RTOption configures a RefreshingToken.

func WithOnRefresh

func WithOnRefresh(fn func(access, refresh string)) RTOption

WithOnRefresh registers a callback invoked with each new (access, refresh) pair. Use it to persist the rotating refresh token.

func WithRefreshBaseURL

func WithRefreshBaseURL(u string) RTOption

WithRefreshBaseURL sets the environment the refresh call is made against (default DefaultBaseURL). Use the same base URL as the Client.

func WithRefreshHTTPClient

func WithRefreshHTTPClient(h *http.Client) RTOption

WithRefreshHTTPClient supplies the *http.Client used for refresh calls.

type RefreshingToken

type RefreshingToken struct {

	// OnRefresh, if set, is called with each new (access, refresh) pair right
	// after a successful refresh. Persist the refresh token here.
	OnRefresh func(access, refresh string)
	// contains filtered or unexported fields
}

RefreshingToken keeps a short-lived access token alive by trading a refresh token for a new one whenever the access token is rejected. It is safe for concurrent use.

Refresh tokens ROTATE: every refresh returns a new refresh token and the old one stops working. Set OnRefresh to persist the new pair, or a restart will fall back to a refresh token the server no longer accepts.

func NewRefreshingToken

func NewRefreshingToken(accessToken, refreshToken string, opts ...RTOption) *RefreshingToken

NewRefreshingToken builds a token source from a stored refresh token. Pass an empty accessToken to force a refresh on first use.

func (*RefreshingToken) Refresh

func (rt *RefreshingToken) Refresh(ctx context.Context) (string, error)

Refresh trades the refresh token for a fresh access token, rotating the refresh token and firing OnRefresh.

func (*RefreshingToken) Token

func (rt *RefreshingToken) Token(ctx context.Context) (string, error)

Token returns the current access token, refreshing first if none is held.

type Session

type Session struct {
	Token        string `json:"token"`
	RefreshToken string `json:"refresh_token"`
	User         User   `json:"user"`
}

Session is what a sign-in returns: a short-lived access token, a rotating refresh token, and the signed-in user.

func VerifyMagicCode

func VerifyMagicCode(ctx context.Context, baseURL, email, code string) (*Session, error)

VerifyMagicCode exchanges the emailed code for a Session. Store the refresh token; it is what keeps your service signed in. baseURL may be "" for DefaultBaseURL.

type StaticToken

type StaticToken string

StaticToken is a TokenSource that always returns the same token. Use it when you already hold a valid access token and manage its lifetime yourself.

func (StaticToken) Token

func (s StaticToken) Token(context.Context) (string, error)

Token implements TokenSource.

type TokenSource

type TokenSource interface {
	Token(ctx context.Context) (string, error)
}

TokenSource yields the bearer token to send on each request.

type ToolResult

type ToolResult struct {
	ToolName   string `json:"tool_name"`
	Method     string `json:"method"`
	URL        string `json:"url"`
	StatusCode int    `json:"status_code"`
	Body       string `json:"body"`
}

ToolResult is the raw upstream response from running a tool.

type User

type User struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	Locale    string    `json:"locale"`
	Timezone  string    `json:"timezone"`
	CreatedAt time.Time `json:"created_at"`
}

User is the signed-in Frege user.

Directories

Path Synopsis
examples
bootstrap command
Command bootstrap performs a one-time Frege sign-in and prints the refresh token your service should store.
Command bootstrap performs a one-time Frege sign-in and prints the refresh token your service should store.
telegrambot command
Command telegrambot is a minimal Telegram bot that runs one Frege tool for every message it receives and replies with the result.
Command telegrambot is a minimal Telegram bot that runs one Frege tool for every message it receives and replies with the result.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL