MCP Integration

The glove-mcp package bridges Model Context Protocol servers into a Glove agent. Hosted MCPs (Notion, Gmail, Linear, GitHub, …) become first-class tools — namespaced, permission-aware, and discoverable mid-conversation. The framework stays agnostic: it knows about bearer tokens, nothing more. OAuth dances, refresh schedules, and credential storage live entirely in your app.

Quick Start

Install the package:

sh
pnpm add glove-mcp

Wire it into a Glove with three pieces:

  1. Catalogue — a static McpCatalogueEntry[] describing every MCP server the app supports. Identical across users.
  2. Adapter — a per-conversation McpAdapter. Holds which entries are active and resolves access tokens.
  3. One callmountMcp(glove, { adapter, entries }) reloads previously active servers and folds in the find_capability discovery tool.
ts
import { Glove, Displaymanager, AnthropicAdapter } from "glove-core";
import { mountMcp, type McpAdapter, type McpCatalogueEntry } from "glove-mcp";

const entries: McpCatalogueEntry[] = [
  {
    id: "notion",
    name: "Notion",
    description: "Read and write Notion pages, databases, comments, and blocks.",
    url: "https://mcp.notion.com/mcp",
  },
];

class MyAdapter implements McpAdapter {
  identifier: string;
  private active = new Set<string>();
  constructor(id: string) { this.identifier = id; }

  async getActive() { return [...this.active]; }
  async activate(id: string) { this.active.add(id); }
  async deactivate(id: string) { this.active.delete(id); }

  async getAccessToken(id: string) {
    const t = process.env[`${id.toUpperCase()}_TOKEN`];
    if (!t) throw new Error(`No token for "${id}"`);
    return t;
  }
}

const glove = new Glove({
  store: new MemoryStore("convo-1"),
  model: new AnthropicAdapter({ model: "claude-sonnet-4.5", stream: true }),
  displayManager: new Displaymanager(),
  systemPrompt: "You are a helpful assistant. Use find_capability to discover external tools.",
  serverMode: true,
  compaction_config: { compaction_instructions: "Summarise." },
});

await mountMcp(glove, { adapter: new MyAdapter("convo-1"), entries });
glove.build();

That's it. The agent boots with find_capability folded in. When the model needs Notion, it calls find_capability("notion"); a discovery subagent matches the catalogue, calls adapter.activate("notion"), connects, and folds bridged tools (notion__search, notion__fetch, …) onto the running Glove. Next turn the model uses them directly.

The McpAdapter

Per-conversation, mirrors StoreAdapter. Five methods. State it holds: which entries are active, and how to resolve a token.

MethodTypePurpose
identifierstringLog-correlation id, typically the conversation id.
getActive() => Promise<string[]>Active entry ids in this conversation. Read by mountMcp at boot for reload.
activate(id) => Promise<void>Mark active. Called by the discovery subagent after a successful connect + fold.
deactivate(id) => Promise<void>Mark inactive. v1 limitation: doesn't unfold tools from the running Glove — refresh the session for that.
getAccessToken(id) => Promise<string>Sole auth seam. Returns a bearer token; framework wraps it as Authorization: Bearer …. Throwing fails activation/reload gracefully.

The state split is clean. Adapter (per-conversation) holds the active set. Token store (often shared) holds credentials. Build a tiny adapter that delegates token reads to whatever store you use — the framework only ever sees the returned string.

McpCatalogueEntry

Static, app-level config — describes one MCP server. Pass an array of entries to mountMcp alongside the adapter. The id doubles as the tool namespace prefix (a Notion search tool surfaces to the model as notion__search; the __ separator is regex-safe across all model providers).

FieldTypePurpose
idstringStable namespace prefix and activation key.
namestringHuman-readable name. Used by the discovery subagent to match user intent.
descriptionstringShort blurb. Discovery matches against this too.
urlstringMCP server URL. v1 supports HTTP transport only.
tagsstring[] (optional)Discovery uses these for matching.
metadataRecord<string, unknown> (optional)Arbitrary extra info, untouched by the framework.

Discovery & find_capability

mountMcp always folds find_capability into the agent. The model calls it with a brief description (e.g. "send an email"); a discovery subagent matches the catalogue, optionally negotiates ambiguity, calls activate(id), and folds the bridged tools. Three ambiguity policies decide what happens when more than one entry matches:

PolicyTypeWhen to use
{ type: "interactive" }default in UI GlovesSubagent calls pushAndWait with an mcp_picker slot. Requires a renderer in your displayManager. The user picks; the subagent activates.
{ type: "auto-pick-best" }default when serverMode: trueSubagent silently picks the highest-ranked match. Use for headless agents, evals, cron jobs.
{ type: "defer-to-main" }explicit opt-inSubagent returns the candidate list as text and lets the main agent decide. Useful when the main model is better-positioned to disambiguate from full conversation context.

Override the subagent's model or system prompt via subagentModel / subagentSystemPrompt on MountMcpConfig if you want.

Auth model

The framework's auth surface is exactly one method: McpAdapter.getAccessToken(id) => Promise<string>. It hands the string to connectMcp as Authorization: Bearer <string>. That's the whole protocol. Where you got the token, how you refresh it, and where you persist it are all your concern.

When a token expires mid-call, the bridged tool returns:

ts
{ status: "error", message: "auth_expired", data: null }

That's the contract. Watch for it in your subscriber, refresh the token in your store, and the next connection picks up the new value. The framework never touches refresh logic.

OAuth helpers (opt-in)

For the common case of running the MCP authorization spec OAuth flow yourself, glove-mcp ships an opt-in subpath:

ts
import {
  runMcpOAuth,
  FsOAuthStore,        // file-backed (atomic writes, mode 0600)
  MemoryOAuthStore,    // in-process (tests / single-shot scripts)
  McpOAuthProvider,    // SDK provider impl, store-backed
  buildClientMetadata,
} from "glove-mcp/oauth";

runMcpOAuth drives the full dance — discovery, Dynamic Client Registration (or pre-registered creds for servers like Google's), PKCE, callback listener, token persist, and a verification call:

ts
import { FsOAuthStore, runMcpOAuth } from "glove-mcp/oauth";

await runMcpOAuth({
  serverUrl: "https://mcp.notion.com/mcp",
  store: new FsOAuthStore(".mcp-oauth.json"),
  key: "notion",
  port: 53683,
});

// For servers that don't support DCR (Google):
await runMcpOAuth({
  serverUrl: "https://gmailmcp.googleapis.com/mcp/v1",
  store: new FsOAuthStore(".mcp-oauth.json"),
  key: "gmail",
  port: 53684,
  preRegisteredClient: {
    client_id: process.env.GMAIL_OAUTH_CLIENT_ID!,
    client_secret: process.env.GMAIL_OAUTH_CLIENT_SECRET!,
  },
  scope: "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.compose",
  verify: { type: "callTool", name: "list_labels" },
});

The acquired access_token is the bearer token — your getAccessToken reads it from the store and hands it back to the framework:

ts
import { FsOAuthStore } from "glove-mcp/oauth";

const STORE = new FsOAuthStore(".mcp-oauth.json");

class MyAdapter implements McpAdapter {
  // ...active-set methods
  async getAccessToken(id: string) {
    const state = await STORE.get(id);
    if (state.tokens?.access_token) return state.tokens.access_token;
    throw new Error("Run `my-app auth " + id + "`");
  }
}

If you have static tokens already (env vars, internal integration secrets, vault reads, your own OAuth from elsewhere), skip glove-mcp/oauth entirely. getAccessToken is the only seam.

Bridged tool semantics

bridgeMcpTool(connection, tool, serverMode) turns each MCP tool into a GloveFoldArgs. mountMcp and the discovery activate tool call this for you; you typically don't invoke it directly.

serverMode

new Glove({ serverMode: true, ... }) is the canonical "I am headless" flag. Two MCP-relevant defaults flip:

Use serverMode for cron agents, eval harnesses, WebSocket bots — anything without a UI to drive permission prompts. UI agents leave it false (the default) so your renderer can intercept destructive calls.

Production lift-and-shift

For multi-user apps, three things change. The agent code does not.

See the examples/mcp-cli/ folder in the repo for a complete reference consumer covering Notion, Gmail, and the multi-MCP discovery agent.