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 registers the discovermcp discovery subagent via glove.defineSubAgent(...).
ts
import { Glove, Displaymanager, MemoryStore } from "glove-core";
import { createAdapter } from "glove-core/models/providers";
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: createAdapter({ provider: "anthropic", stream: true }),
  displayManager: new Displaymanager(),
  systemPrompt:
    "You are a helpful assistant. When you need a capability you don't have, " +
    "invoke the discovermcp subagent via glove_invoke_subagent.",
  serverMode: true,
  compaction_config: { compaction_instructions: "Summarise." },
}).build();

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

That's it. After mountMcp returns, the agent has the framework's built-in glove_invoke_subagent dispatch tool with discovermcp in its registry. When the model needs Notion, it calls glove_invoke_subagent({ name: "discovermcp", prompt: "Find an MCP for Notion" }); the 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 — the discovermcp subagent

mountMcp registers a subagent named discovermcp on the running Glove via glove.defineSubAgent(discoverySubAgent({...})). The framework auto-registers the glove_invoke_subagent dispatch tool the first time you define a subagent, so the model invokes discovery with glove_invoke_subagent({ name: "discovermcp", prompt }). The subagent runs in an isolated context — its own store, its own message history, its own system prompt — and matches the catalogue, negotiates ambiguity, calls activate(id), and folds bridged tools onto the parent Glove. 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 via the parent's display manager. Requires a renderer for that key. 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; otherwise both are inherited from the parent Glove at invocation time.

Wiring discovery without mountMcp

mountMcp is a thin convenience over discoverySubAgent(config) — call it directly when you want to defer reload, skip it entirely, or combine the discovery subagent with other custom subagents:

ts
import { discoverySubAgent, type DiscoverySubAgentConfig } from "glove-mcp";

const config: DiscoverySubAgentConfig = {
  adapter,
  entries,
  ambiguityPolicy: { type: "interactive" },
};

glove.defineSubAgent(discoverySubAgent(config));

The factory builds a child Glove per invocation, asking the parent store for an isolated child store via parentStore.createSubAgentStore?.("discovermcp", false) (falling back to in-memory when the parent store doesn't implement sub-stores). The child uses the parent's displayManager so ask_user picker slots surface in the parent's UI.

Observability

Every discovermcp invocation is bracketed with matching subagent_invoked and subagent_completed subscriber events — even when the run aborts or errors out. While the subagent is running, parent subscribers are temporarily attached to the child Glove, so model deltas, tool uses, and tool results from the discovery loop flow through the same subscriber pipeline as the parent. Use the brackets to separate parent activity from nested subagent activity in logs and analytics.

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.

  • Naming ${connection.namespace}__${tool.name}. Notion's remote search becomes notion__search. The __ separator is safe across providers.
  • Schema — the MCP server's inputSchema (raw JSON Schema) is forwarded verbatim via the new Tool.jsonSchema field. The executor skips local Zod validation; the server is the source of truth.
  • Permission gating — derived from the MCP tool's annotations and your serverMode. When serverMode === true, all bridged tools default to requiresPermission: false. Otherwise, tools are gated unless their MCP annotation has readOnlyHint: true.
  • renderData — the full MCP content[] array (text, images, resources) is passed through as renderData on the tool result. Server-side agents ignore it; React renderers can use it for rich display. The model only ever sees the joined text in data.
  • auth_expired — 401 / unauthorized responses during a tool call map to { status: "error", message: "auth_expired", data: null }.

serverMode

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

  • Bridged tools never gate. Even MCP-annotated destructiveHint: true tools execute without permission prompts (no user to ask).
  • Discovery's ambiguity policy defaults to auto-pick-best.

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.

  • Token store — replace FsOAuthStore with a per-user implementation backed by your DB. The OAuthStore interface is three methods (get, set, delete).
  • OAuth flow — move runMcpOAuth from a CLI into route handlers. GET /oauth/<id>/start redirects; GET /oauth/<id>/callback exchanges. Same machinery.
  • Refresh — background-refresh expired tokens however your stack does it. The agent reads bearer strings from your store via getAccessToken on every connection, so updating the store is enough.

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

  • Server-Side Agents — the headless context most MCP agents run in.
  • Core Concepts — Glove's building blocks (Tools, Adapters, Display Stack).
  • Core API Glove builder, fold, build, processRequest.