Tools, MCP and memory
Foundry has three ways to give an agent abilities, and they differ in who decides: the definition decides for every instance, an instance installs from a typed catalogue, or the definition selects a native Glove surface such as memory. Picking the right tier is what lets one agent definition serve many customers with different integrations.
Which tier?
| You want | Use | Mounted when | Configured by |
|---|---|---|---|
| A tool every instance has | tools field | Every run (a resolver can filter per message) | Code |
| A tool only some instances have, with per-instance settings | defineSharedTool + install | The instance has an installation for it | Installation config (zod) |
| A bundle of tools and inbound/outbound transports | defineApp + install | Installed | Installation config and account |
| An external MCP server | defineMcp + install + mcpAdapter | Installed | entry / config; auth in the adapter |
| Memory (context, entities, episodes, resources) | defineMemory + memory field | The definition selects it | configureMemory |
| Raw access to the Glove agent each run | defineLayer + layers field | Selected | configureLayer |
| Observe model and tool events | defineSubscriber + subscribers | Selected | Code |
A typed function the model or your run can call | defineCall + calls | Listed | Code |
| Hooks, skills, subagents | hooks, skills, subagents | Listed | Code |
composeAgent(...) in components is only a catalogue. Shared tools, apps and MCP servers listed there are not mounted until an instance installs them. Memory profiles, layers and Foundry subscribers must be in the catalogue and selected in the memory, layers or subscribers field; a selection missing from the catalogue fails the run with “unknown memory profile/layer/subscriber”.
Always-on tools
The tools field takes plain Glove GloveFoldArgs objects (name, description, inputSchema or jsonSchema, do, optional requiresPermission). Use a literal array for a fixed set or a resolver to vary it per message; the assembly page has a full example. Every instance gets these tools; nothing has to be installed.
Installable capabilities
Installables are colocated files, one default export each. The file name is the id: agents/support/tools/lookup-order.tool.ts is the shared tool lookup-order.
defineSharedTool
Give either a static tool, or create(ctx) returning an Effect of a tool, built per run from the installation's decoded config.
import { Effect } from "effect";
import { defineSharedTool } from "glove-foundry";
import { z } from "zod";
const lookupOrder = defineSharedTool({
description: "Look up an order in a regional order system",
config: z.object({
region: z.enum(["us", "eu"]),
includeHistory: z.boolean().default(false),
}),
create: ({ config, agentId }) =>
Effect.succeed({
name: "lookup_order",
description: `Look up an order in the ${config.region.toUpperCase()} order system`,
inputSchema: z.object({ orderId: z.string() }),
async do({ orderId }) {
// your code: call the order API for config.region
return { status: "success" as const, data: { orderId, region: config.region, agentId } };
},
}),
});
export default lookupOrder;create receives the run's identity and message fields, the Glove agent, the store, installation and typed config. When the installation selected an account it also gets accountId, plus withAccountSession if the agent defines accountSessions. A *.tool.ts may instead default-export a bare tool body (no name); composeAgent wraps it and names the tool from the file (lookup-order → lookup_order).
defineApp
An app is an installable bundle. Its install callback may contribute tools, its outbound transmissions become tools, and its inbound transmissions become available to the instance's playbooks. The install context deliberately has no Glove agent or store.
import { Effect } from "effect";
import { defineApp } from "glove-foundry";
import { z } from "zod";
const helpdesk = defineApp({
description: "Read and update tickets in one helpdesk queue",
config: z.object({ queue: z.string(), retries: z.number().default(2) }),
// inbound: [tickets], outbound: [tickets] — see Apps & transmissions
install: ({ config }) =>
Effect.succeed({
tools: [{
name: "helpdesk_queue",
description: "Return the helpdesk queue this agent works",
inputSchema: z.object({}),
async do() {
return { status: "success" as const, data: { queue: config.queue, retries: config.retries } };
},
}],
}),
});
export default helpdesk;Transmissions, accounts, routes and connections are covered in Apps & transmissions.
defineMcp
An MCP definition is a catalogue entry for one server, over HTTP or a local stdio process. Its tools appear as <mcp id>__<server tool>, for example docs-search__search_pages.
import { defineMcp } from "glove-foundry";
const docsSearch = defineMcp({
description: "Search the public product documentation",
entry: {
name: "Docs search",
description: "Search and read product documentation pages",
url: "https://mcp.example.com/docs",
includeTools: ["search_*", "read_page"],
prompts: false,
requestTimeoutMs: 30_000,
},
});
export default docsSearch;import { defineMcp } from "glove-foundry";
const kb = defineMcp({
description: "Internal knowledge base over stdio",
entry: {
name: "Knowledge base",
description: "Read internal support articles",
transport: { kind: "stdio", command: "/opt/mcp/kb-server", args: ["--stdio"] },
excludeTools: ["delete_*", "write_*"],
connectTimeoutMs: 15_000,
idleTimeoutMs: 15 * 60_000,
maxLifetimeMs: 24 * 60 * 60_000,
},
});
export default kb;entry can also be a function of the installation's typed config, so each instance can point at a different server:
import { defineMcp } from "glove-foundry";
import { z } from "zod";
const tracker = defineMcp({
description: "Issue tracker scoped to one project",
config: z.object({ project: z.string() }),
entry: ({ config }) => ({
name: "Issue tracker",
description: `Issues for project ${config.project}`,
url: `https://tracker.example.com/mcp?project=${encodeURIComponent(config.project)}`,
}),
});
export default tracker;| entry field | Meaning |
|---|---|
name, description | Required. Shown to glove-mcp's discovery subagent. |
url | HTTP endpoint (short form). Use either url or transport. |
transport | { kind: "http", url } or { kind: "stdio", command, args?, cwd? } |
includeTools | Exact server tool names or globs. A non-empty list is an allowlist. |
excludeTools | Exact names or globs to hide, un-namespaced ("delete_repo", not "kb__delete_repo"). |
resources, prompts | Mount list/read resource and list/get prompt utility tools. Both default to true. |
connectTimeoutMs | Bound on connect/initialize. Default 30 s. |
requestTimeoutMs | Bound on every MCP operation. Default 60 s. |
idleTimeoutMs, maxLifetimeMs | Stdio only: close an idle child / recycle after a lifetime; reopened on demand. 0 disables. |
tags, metadata | Optional discovery tags and arbitrary metadata. |
Do not put an id in entry; Foundry uses the file id. Credentials never belong in entry or installation config. They come from the agent's mcpAdapter, which Foundry calls once per run that has MCP installations. An instance that installs an MCP definition fails to assemble if the agent has no mcpAdapter.
import { Effect } from "effect";
import { defineAgent } from "glove-foundry";
export default defineAgent({
// ...description, components, model, systemPrompt
mcpAdapter: ({ agentId, resolved }) =>
Effect.succeed({
identifier: `support:${agentId}`,
getActive: async () => resolved.map((item) => item.definition.id),
activate: async () => {},
deactivate: async () => {},
// HTTP: headers (or getAccessToken for a bearer token), resolved per connection
getAuthHeaders: async (id: string): Promise<Record<string, string>> =>
id === "docs-search" ? { authorization: `Bearer ${process.env.DOCS_MCP_TOKEN ?? ""}` } : {},
// stdio: environment for the child process, resolved at every (re)spawn
getStdioEnvironment: async (id: string): Promise<Record<string, string>> =>
id === "kb" ? { KB_TOKEN: process.env.KB_TOKEN ?? "" } : {},
}),
});- The factory receives the run context plus
installed(MCP definitions) andresolved({ definition, installation, config, entry }per installed server). - Foundry overrides
getActivewith the installed ids, so every installed server is connected at assembly. Glove-mcp's discovery subagent is also registered. - Connecting fails open: a server that cannot be reached is skipped and the run continues without its tools.
getAuthHeaderstakes precedence overgetAccessToken. Foundry never acquires or refreshes tokens; your adapter does. See glove-mcp.
Put them in the catalogue
import { composeAgent } from "glove-foundry";
import helpdesk from "./apps/helpdesk.app.js";
import tenantContext from "./layers/tenant.layer.js";
import docsSearch from "./mcp/docs-search.mcp.js";
import kb from "./mcp/kb.mcp.js";
import tracker from "./mcp/tracker.mcp.js";
import customerMemory from "./memory/customer.memory.js";
import audit from "./subscribers/audit.subscriber.js";
import lookupOrder from "./tools/lookup-order.tool.js";
export const components = composeAgent(
lookupOrder, helpdesk, docsSearch, kb, tracker, // installable per instance
customerMemory, tenantContext, audit, // selected by the definition
);Then components goes on the definition: defineAgent({ components, ... }).
Installing on an instance
install(definition, config?, { account? }) produces an AgentInstallation record: { kind: "tool" | "application" | "mcp", id, config?, accountId? }. Install at creation in code:
import { defineAgentInstance, install } from "glove-foundry";
import support from "./agent.js";
import helpdesk from "./apps/helpdesk.app.js";
import docsSearch from "./mcp/docs-search.mcp.js";
import tracker from "./mcp/tracker.mcp.js";
import lookupOrder from "./tools/lookup-order.tool.js";
export const acmeSupport = defineAgentInstance(support, {
id: "support-acme",
workspaceId: "acme",
context: { tenant: "Acme", canRefund: true },
installations: [
install(lookupOrder, { region: "eu" }), // includeHistory defaults to false
install(helpdesk, { queue: "billing" }),
install(docsSearch), // no config schema, no config
install(tracker, { project: "SUP" }),
],
});Register the instance with your data adapter (see seeding instances). At runtime, from a client or over HTTP, use plain records with string ids:
import { createFoundryClient } from "glove-foundry/client";
import type { FoundryRoutes } from "../.foundry/routes.js";
const foundry = createFoundryClient<FoundryRoutes>({ baseUrl: "http://127.0.0.1:4141" });
const agent = await foundry.agent("support").create({
workspaceId: "globex",
installations: [{ kind: "application", id: "helpdesk", config: { queue: "sales" } }],
});
await foundry.install(agent.id, { kind: "tool", id: "lookup-order", config: { region: "us" } });
await foundry.uninstall(agent.id, { kind: "application", id: "helpdesk" });
console.log(await foundry.installations(agent.id));PUT /api/installations
content-type: application/json
{ "agentId": "support-acme", "kind": "mcp", "id": "tracker", "config": { "project": "OPS" } }
DELETE /api/installations
content-type: application/json
{ "agentId": "support-acme", "kind": "mcp", "id": "tracker" }
GET /api/installations?agent=support-acme- Install checks that
kind+idexist in that definition'scomponents; otherwise it fails with “Unknown Foundry tool capability”. - Config is decoded against the zod schema when a run assembles, not at install time. Invalid persisted config fails the next run (“Invalid config for tool …”).
- One installation per
kind+id: installing again replaces the earlier config. - Installation changes apply from the next run. Uninstalling an app that one of the instance's playbooks needs is rejected.
- Select an account with
install(def, config, { account })in code, or anaccountIdin thePUT /api/installationsbody or inPATCH /api/agent-instances/:idinstallations. Config may not contain anaccountIdkey. - The inspector shows a definition's catalogue and an instance's installations, read-only. Install through code, the client or HTTP.
Typed config
The zod schema on a definition types install's second argument (zod input, so defaulted fields are optional) and the config your create/install/entry receives (zod output):
install(helpdesk, { queue: "billing" }); // ok: retries defaults to 2
// @ts-expect-error queue must be a string
install(helpdesk, { queue: 42 });
// @ts-expect-error lookupOrder declares a config schema, so config is required
install(lookupOrder);
// @ts-expect-error docsSearch has no config schema
install(docsSearch, { anything: true });Definition surfaces: memory, layers, subscribers
These are not installations. The definition selects them, optionally per message, and they mount for every run that selects them.
defineMemory
import { Effect } from "effect";
import { defineMemory } from "glove-foundry";
import { MemorySchema } from "glove-memory/core";
import { InMemoryContextAdapter, InMemoryEntityAdapter } from "glove-memory/in-memory";
const schema = new MemorySchema();
const customerMemory = defineMemory({
description: "What we know about customers and this conversation",
context: {
adapter: ({ agentId }) =>
Effect.succeed(new InMemoryContextAdapter({ schema, identifier: `support-context:${agentId}` })),
},
entity: {
access: "curator", // read + write tools; "reader" (default) is read-only
adapter: () => Effect.succeed(new InMemoryEntityAdapter({ schema })),
},
});
export default customerMemory;| Key | Adapter returned by adapter(ctx) | Mounts |
|---|---|---|
context | ContextAdapter | Ambient context on the main agent (useContext) |
entity | EntityMemoryAdapter | Entity reader or curator tools |
episodic | EpisodicMemoryAdapter | Episodic reader or curator tools |
resources | ResourceFsAdapter | Resource reader or curator tools |
mount | (function returning Effect<void>) | Anything else, with the Glove agent in hand |
At least one key is required. entity, episodic and resources accept access and a tools option; context accepts tools. Each adapter factory gets the run context, the Glove agent, the store and the profile's decoded config. With a config schema, select it with configureMemory(profile, config) (typed at the call site; the factory sees config as unknown). In-memory adapters are recreated in every run's process; use durable adapters from glove-memory for anything that must persist.
defineLayer and configureLayer
A layer's setup receives the native Glove agent as glove plus the run context and typed config, returns an Effect, and may return a cleanup function.
import { Effect } from "effect";
import { defineLayer } from "glove-foundry";
import { z } from "zod";
const tenantContext = defineLayer({
description: "Pin the tenant name into every model iteration",
config: z.object({ tenant: z.string() }),
setup: ({ glove, config }) =>
Effect.sync(() => {
const remove = glove.addContextProvider(() => `Tenant: ${config.tenant}`);
return remove; // cleanup when the run ends
}),
});
export default tenantContext;defineSubscriber
import { defineSubscriber } from "glove-foundry";
const audit = defineSubscriber({
description: "Record every tool result as a custom run event",
create: ({ emit }) => ({
async record(type) {
if (type === "tool_use_result") emit({ type: "support.audit.tool-result" });
},
}),
});
export default audit;Selecting them on the definition
import { createAdapter } from "glove-core/models/providers";
import { configureLayer, defineAgent } from "glove-foundry";
import { components } from "./composition.js";
import tenantContext from "./layers/tenant.layer.js";
import customerMemory from "./memory/customer.memory.js";
import audit from "./subscribers/audit.subscriber.js";
import { FileStore } from "../../lib/file-store.js"; // durable transcript — see Persistence
export default defineAgent({
description: "Answers customer questions about orders",
components,
store: ({ conversationId }) => FileStore.open(conversationId),
model: () => createAdapter({ provider: "openrouter", model: "openai/gpt-4.1-mini", stream: true }),
systemPrompt: (_agent, { installations }) =>
`You are a support agent. Installed: ${installations.map((i) => `${i.kind}:${i.id}`).join(", ") || "nothing"}.`,
memory: [customerMemory],
layers: (_agent, { agentInstance }) => [
configureLayer(tenantContext, { tenant: String(agentInstance.context.tenant ?? "default") }),
],
subscribers: [
audit,
// a plain Glove SubscriberAdapter needs no catalogue entry
{ async record(type) { if (type === "model_response_complete") console.log("model turn done"); } },
],
});hooks, skills and subagents are also plain definition fields taking Glove's { name, handler } hooks, DefineSkillArgs and DefineSubAgentArgs (build subagents with defineSubagent; see Multi-agent).
Typed calls
defineCall declares a function with zod input and output. By default the model sees it as a tool named after the call; with exposeToAgent: false it is only reachable from code through ctx.invoke.
import { defineAgent, defineCall } from "glove-foundry";
import { z } from "zod";
export default defineAgent({
// ...description, components, model, systemPrompt
calls: [
defineCall({
name: "escalate",
description: "Escalate the conversation to a human with a reason",
input: z.object({ reason: z.string().min(5) }),
output: z.object({ ticketId: z.string() }),
handler: ({ reason }, ctx) => ({ ticketId: `esc-${ctx.conversationId}-${reason.length}` }),
}),
defineCall({
name: "audit_note",
description: "Internal: record an audit note",
input: z.object({ note: z.string() }),
output: z.object({ ok: z.literal(true) }),
exposeToAgent: false,
handler: () => ({ ok: true as const }),
}),
],
run: async (_agent, ctx) => {
await ctx.invoke("audit_note", { note: `run ${ctx.runId}` });
return ctx.defaultRun();
},
});- Names must match
/^[A-Za-z][A-Za-z0-9_-]*$/and be unique per run. invokeparses the input and the handler's output with the schemas; a failure throws (or, as a tool, returns an error result to the model).- Handlers receive the execution context, including
invoke, so calls can compose. - Calls are run-local: there is no HTTP endpoint for them. Other agents reach an agent through its runs, mesh or subagents (see Multi-agent).
Seeing what is available and installed
| Endpoint | Client | Returns |
|---|---|---|
GET /api/capabilities?definition=support | capabilities("support") | { tools, applications, mcp, memory }, each entry { id, kind, description, ownership }; ownership is instance for installables, definition for memory |
GET /api/surfaces?definition=support | surfaces("support") | { layers, subscribers } in the catalogue |
GET /api/installations?agent=<id> | installations(id) | The instance's installation records |
GET /api/manifest | manifest() | All of the above per definition, plus static tool, call and memory names and the list of lazy fields |
In the inspector, an agent definition page shows the Capability catalogue and Native composition cards, and an instance page shows Installed capabilities. Run traces record foundry.installation.completed, foundry.definition.memory.mounted, foundry.layer.mounted and foundry.subscriber.mounted events for every run.