Skip to content

Use Forkmate from your own code

Forkmate is a standard remote MCP server, so anything that speaks MCP can use it. There are no API keys or personal access tokens: your code signs in with OAuth, the same Forkmate sign-in every AI client uses. Listing the tools is open; every tool call acts on the signed-in user’s diary.

No Forkmate account yet? Create one free first. It takes under a minute.

Set up Forkmate in Developers

https://mcp.forkmate.ai/

Forkmate’s server URL. Streamable HTTP, OAuth sign-in, no API key.

  1. 1

    Sign in with OAuth

    Use the official MCP SDK’s OAuth support. It reads the sign-in details from the server, registers your app, and opens the Forkmate sign-in in your browser. Sign in and approve once. Nothing to copy from a settings page: there isn’t one.

  2. 2

    Let the SDK keep the token fresh

    Access tokens last 300 seconds. Request offline_access and the SDK swaps the refresh token for a new access token when it needs one. The scripts below save both to ~/.forkmate-oauth.json so the next run skips the browser. Treat that file like a password.

  3. 3

    Call the tools

    List the tools, then call log_meal, get_day, search_foods and the rest. Each call acts on the signed-in user’s diary only.

  4. 4

    Using a hosted API? Pass a fresh token

    The Anthropic and OpenAI APIs call Forkmate from their own servers and can’t open a browser, so you hand them an access token on every request. Sign in once with the Python script, then use forkmate_token.py to refresh one before each call.

Python, official MCP SDK with OAuth (mcp 2.2)

# pip install "mcp>=2.2"
import asyncio, json, pathlib, webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse

import httpx2
from pydantic import AnyUrl
from mcp import Client
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider, TokenStorage
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken

SERVER = "https://mcp.forkmate.ai/"
REDIRECT = "http://localhost:8765/callback"
STORE = pathlib.Path.home() / ".forkmate-oauth.json"


class FileTokenStorage(TokenStorage):
    """Keeps the tokens and the client registration between runs."""

    def _load(self):
        return json.loads(STORE.read_text()) if STORE.exists() else {}

    def _save(self, key, model):
        data = self._load()
        data[key] = model.model_dump(mode="json", exclude_none=True)
        STORE.write_text(json.dumps(data))
        STORE.chmod(0o600)

    async def get_tokens(self):
        t = self._load().get("tokens")
        return OAuthToken.model_validate(t) if t else None

    async def set_tokens(self, tokens: OAuthToken):
        self._save("tokens", tokens)

    async def get_client_info(self):
        c = self._load().get("client")
        return OAuthClientInformationFull.model_validate(c) if c else None

    async def set_client_info(self, client_info: OAuthClientInformationFull):
        self._save("client", client_info)


async def open_browser(url: str) -> None:
    webbrowser.open(url)


async def wait_for_callback() -> AuthorizationCodeResult:
    result = {}

    class Handler(BaseHTTPRequestHandler):
        def do_GET(self):
            result.update({k: v[0] for k, v in parse_qs(urlparse(self.path).query).items()})
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b"Signed in. You can close this tab.")

        def log_message(self, *args):
            pass

    server = HTTPServer(("localhost", 8765), Handler)
    await asyncio.to_thread(server.handle_request)
    server.server_close()
    return AuthorizationCodeResult(code=result["code"], state=result.get("state"), iss=result.get("iss"))


async def main():
    auth = OAuthClientProvider(
        server_url=SERVER,
        client_metadata=OAuthClientMetadata(
            client_name="My Forkmate script",
            redirect_uris=[AnyUrl(REDIRECT)],
            grant_types=["authorization_code", "refresh_token"],
            scope="openid profile email offline_access",
        ),
        storage=FileTokenStorage(),
        redirect_handler=open_browser,
        callback_handler=wait_for_callback,
    )
    async with httpx2.AsyncClient(auth=auth) as http:
        async with Client(streamable_http_client(SERVER, http_client=http)) as client:
            tools = await client.list_tools()
            print([t.name for t in tools.tools])
            print((await client.call_tool("get_day", {})).content)

asyncio.run(main())

The SDK defines TokenStorage and calls it; where the tokens live is up to you. This one is a file readable only by you; an OS keychain is better for anything long-lived. The browser opens on the first run only.

TypeScript, official MCP SDK with OAuth (@modelcontextprotocol/client 2.2)

// npm i @modelcontextprotocol/client   (Node 20+, an ES module: save as forkmate.mts)
import { createServer } from "node:http";
import { execFile } from "node:child_process";
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import {
  Client,
  StreamableHTTPClientTransport,
  UnauthorizedError,
  type OAuthClientProvider,
} from "@modelcontextprotocol/client";

const SERVER = new URL("https://mcp.forkmate.ai/");
const REDIRECT = "http://localhost:8090/callback";
const STORE = join(homedir(), ".forkmate-oauth.json");

// The SDK runs the flow; the provider stores what it's handed and opens the browser.
const saved = existsSync(STORE) ? JSON.parse(readFileSync(STORE, "utf8")) : {};
const persist = () => {
  writeFileSync(STORE, JSON.stringify(saved));
  chmodSync(STORE, 0o600);
};
let verifier = "";
let discovery: any;
let lastState = "";

const provider: OAuthClientProvider = {
  redirectUrl: REDIRECT,
  clientMetadata: {
    client_name: "My Forkmate script",
    redirect_uris: [REDIRECT],
    grant_types: ["authorization_code", "refresh_token"],
    scope: "openid profile email offline_access",
  },
  state: () => (lastState = crypto.randomUUID()),
  clientInformation: (ctx) => (ctx ? saved.clients?.[ctx.issuer] : undefined),
  saveClientInformation: (info, ctx) => {
    if (ctx) (saved.clients ??= {})[ctx.issuer] = info;
    persist();
  },
  tokens: () => saved.tokens,
  saveTokens: (tokens) => {
    saved.tokens = tokens;
    persist();
  },
  saveCodeVerifier: (v) => void (verifier = v),
  codeVerifier: () => verifier,
  saveDiscoveryState: (s) => void (discovery = s),
  discoveryState: () => discovery,
  redirectToAuthorization: (url) => {
    console.log("Sign in to Forkmate:", url.href);
    const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
    execFile(opener, [url.href], () => {});
  },
};

// One request to the loopback redirect, carrying ?code=…&state=…&iss=…
const waitForCallback = () =>
  new Promise<URLSearchParams>((resolve) => {
    const server = createServer((req, res) => {
      res.end("Signed in. You can close this tab.");
      server.close();
      resolve(new URL(req.url!, REDIRECT).searchParams);
    }).listen(8090, "localhost");
  });

// A fresh client and transport per attempt: a started transport can't restart.
async function run() {
  const client = new Client({ name: "my-app", version: "1.0.0" });
  await client.connect(new StreamableHTTPClientTransport(SERVER, { authProvider: provider }));
  try {
    console.log((await client.listTools()).tools.map((t) => t.name));
    console.log((await client.callTool({ name: "get_day", arguments: {} })).content);
  } finally {
    await client.close();
  }
}

try {
  await run();
} catch (error) {
  // No usable token: the SDK has opened the sign-in. Finish it, then run again.
  if (!(error instanceof UnauthorizedError)) throw error;
  const params = await waitForCallback();
  if (params.get("state") !== lastState) throw new Error("state mismatch");
  await new StreamableHTTPClientTransport(SERVER, { authProvider: provider }).finishAuth(params);
  await run();
}

Run it with npx tsx forkmate.mts. Written for SDK v2; the v1 package (@modelcontextprotocol/sdk) has a different OAuth API. The SDK’s own guide recommends an OS keychain over a plain file for real apps.

Go, official SDK with OAuth (github.com/modelcontextprotocol/go-sdk v1.8)

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/modelcontextprotocol/go-sdk/auth"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/modelcontextprotocol/go-sdk/oauthex"
)

const redirect = "http://localhost:3142/callback"

// Prints the sign-in URL and waits for the browser to come back to the loopback redirect.
func fetchCode(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
	fmt.Println("Sign in to Forkmate:", args.URL)
	got := make(chan *auth.AuthorizationResult, 1)
	srv := &http.Server{Addr: "localhost:3142", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		fmt.Fprint(w, "Signed in. You can close this tab.")
		got <- &auth.AuthorizationResult{Code: q.Get("code"), State: q.Get("state"), Iss: q.Get("iss")}
	})}
	go srv.ListenAndServe()
	defer srv.Close()
	select {
	case res := <-got:
		return res, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

func main() {
	ctx := context.Background()
	oauth, err := auth.NewAuthorizationCodeHandler(&auth.AuthorizationCodeHandlerConfig{
		RedirectURL:              redirect,
		AuthorizationCodeFetcher: fetchCode,
		RequestRefreshToken:      true,
		DynamicClientRegistrationConfig: &auth.DynamicClientRegistrationConfig{
			Metadata: &oauthex.ClientRegistrationMetadata{
				ClientName:   "My Forkmate script",
				RedirectURIs: []string{redirect},
				GrantTypes:   []string{"authorization_code", "refresh_token"},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	transport := &mcp.StreamableClientTransport{Endpoint: "https://mcp.forkmate.ai/", OAuthHandler: oauth}
	client := mcp.NewClient(&mcp.Implementation{Name: "my-app", Version: "v1.0.0"}, nil)
	session, err := client.Connect(ctx, transport, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	tools, err := session.ListTools(ctx, nil)
	if err != nil {
		log.Fatal(err)
	}
	for _, t := range tools.Tools {
		fmt.Println(t.Name)
	}
	res, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "get_day", Arguments: map[string]any{}})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Content)
}

The Go SDK keeps the token in memory and refreshes it while the program runs, so each new run signs in again. To persist it, pass your own NewTokenSource and InitialTokenSource in AuthorizationCodeHandlerConfig.

Hosted APIs, step 1: forkmate_token.py (a fresh access token)

# forkmate_token.py: a fresh access token from the sign-in the Python script saved.
import json, pathlib
import httpx  # installed with anthropic and openai

STORE = pathlib.Path.home() / ".forkmate-oauth.json"


def access_token() -> str:
    saved = json.loads(STORE.read_text())
    prm = httpx.get("https://mcp.forkmate.ai/.well-known/oauth-protected-resource").json()
    issuer = prm["authorization_servers"][0]
    token_url = httpx.get(f"{issuer}/.well-known/oauth-authorization-server").json()["token_endpoint"]
    r = httpx.post(token_url, data={
        "grant_type": "refresh_token",
        "refresh_token": saved["tokens"]["refresh_token"],
        "client_id": saved["client"]["client_id"],
        "resource": prm["resource"],
    })
    r.raise_for_status()
    saved["tokens"] = r.json()
    STORE.write_text(json.dumps(saved))  # refresh tokens rotate: keep the new one
    return saved["tokens"]["access_token"]

Run the Python SDK script once first; it signs you in and saves the refresh token this reads. Just testing? The MCP Inspector’s Quick OAuth Flow (npx @modelcontextprotocol/inspector, then Open Auth Settings) also signs you in and shows an access_token. It lasts 300 seconds and you can’t refresh it from here.

Hosted APIs, step 2: Anthropic API, MCP connector (Python, anthropic 1.11)

import anthropic
from forkmate_token import access_token

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

response = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=16000,
    betas=["mcp-client-2025-11-20"],
    mcp_servers=[{
        "type": "url",
        "url": "https://mcp.forkmate.ai/",
        "name": "forkmate",
        "authorization_token": access_token(),
    }],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "forkmate"}],
    messages=[{"role": "user", "content": "Log 2 eggs and a slice of toast for breakfast."}],
)
print(response.content)

Pass the raw token, without “Bearer ”. Anthropic expects you to run the OAuth flow and refresh the token yourself. Every server in mcp_servers needs exactly one mcp_toolset entry in tools.

Hosted APIs, step 2: OpenAI Responses API, remote MCP tool (Python, openai 3.22)

from openai import OpenAI
from forkmate_token import access_token

client = OpenAI()
resp = client.responses.create(
    model="gpt-6-astra",
    input="What did I eat today?",
    tools=[{
        "type": "mcp",
        "server_label": "forkmate",
        "server_url": "https://mcp.forkmate.ai/",
        "authorization": access_token(),
        "require_approval": "never",
    }],
)
print(resp.output_text)

OpenAI doesn’t store the authorization value, so send it on every request.

curl: initialize and list tools (no sign-in needed)

curl -s https://mcp.forkmate.ai/ \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

curl -s https://mcp.forkmate.ai/ \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-06-18" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# A tool call needs an OAuth sign-in. Without a token the server answers 401,
# with WWW-Authenticate pointing at its OAuth metadata: that's where the SDKs start.
curl -s -i https://mcp.forkmate.ai/ \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-06-18" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'

The server is stateless: it sends no Mcp-Session-Id, and answers in plain JSON. curl can’t do the browser sign-in; use an SDK above for tool calls.

What you need

  • A free Forkmate account (sign up). There is no paid plan.
  • No API keys or personal access tokens. Every tool call needs an OAuth access token from a signed-in Forkmate user, through the standard MCP flow (authorization code with PKCE, resource=https://mcp.forkmate.ai). Access tokens last 300 seconds; request offline_access for a refresh token.
  • Logging a meal needs the user’s one-time health-data consent, given in the web app. Until then log_meal and update_meal return “Nothing was saved” and a link.
  • Writes are limited to 60 a minute per user.

Checked October 1, 2026 against these sources: MCP Python SDK: OAuth client example, MCP TypeScript SDK: OAuth, MCP Go SDK: OAuth client example, Anthropic: MCP connector, OpenAI: MCP and connectors. SDKs move fast; check the version you install.

What to say first

Your users talk to your app the way they’d tell a friend:

Log 2 eggs and a slice of toast for breakfast.

It logs the meal with calories, protein, carbs and fat to your diary at app.forkmate.ai. Then try:

  • How many calories have I had today?
  • Make the toast two slices.
  • What did I eat this week?

Something not working? See troubleshooting, orpick a different assistant. The same URL works in all of them.

Build on a free food diary

Create your free account, connect once, and your next meal is one sentence away. No card, no paid plan.