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.
Install the package:
pnpm add glove-mcpWire it into a Glove with three pieces:
McpCatalogueEntry[] describing every MCP server the app supports. Identical across users.McpAdapter. Holds which entries are active and resolves access tokens.mountMcp(glove, { adapter, entries }) reloads previously active servers and folds in the find_capability discovery tool.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.
Per-conversation, mirrors StoreAdapter. Five methods. State it holds: which entries are active, and how to resolve a token.
| Method | Type | Purpose |
|---|---|---|
| identifier | string | Log-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.
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).
| Field | Type | Purpose |
|---|---|---|
| id | string | Stable namespace prefix and activation key. |
| name | string | Human-readable name. Used by the discovery subagent to match user intent. |
| description | string | Short blurb. Discovery matches against this too. |
| url | string | MCP server URL. v1 supports HTTP transport only. |
| tags | string[] (optional) | Discovery uses these for matching. |
| metadata | Record<string, unknown> (optional) | Arbitrary extra info, untouched by the framework. |
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:
| Policy | Type | When to use |
|---|---|---|
| { type: "interactive" } | default in UI Gloves | Subagent 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: true | Subagent silently picks the highest-ranked match. Use for headless agents, evals, cron jobs. |
| { type: "defer-to-main" } | explicit opt-in | Subagent 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.
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:
{ 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.
For the common case of running the MCP authorization spec OAuth flow yourself, glove-mcp ships an opt-in subpath:
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:
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:
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.
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.
${connection.namespace}__${tool.name}. Notion's remote search becomes notion__search. The __ separator is safe across providers.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.serverMode. When serverMode === true, all bridged tools default to requiresPermission: false. Otherwise, tools are gated unless their MCP annotation has readOnlyHint: true.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.{ status: "error", message: "auth_expired", data: null }.new Glove({ serverMode: true, ... }) is the canonical "I am headless" flag. Two MCP-relevant defaults flip:
destructiveHint: true tools execute without permission prompts (no user to ask).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.
For multi-user apps, three things change. The agent code does not.
FsOAuthStore with a per-user implementation backed by your DB. The OAuthStore interface is three methods (get, set, delete).runMcpOAuth from a CLI into route handlers. GET /oauth/<id>/start redirects; GET /oauth/<id>/callback exchanges. Same machinery.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.
Glove builder, fold, build, processRequest.