Build agents / Tools, MCP & memory

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 wantUseMounted whenConfigured by
A tool every instance hastools fieldEvery run (a resolver can filter per message)Code
A tool only some instances have, with per-instance settingsdefineSharedTool + installThe instance has an installation for itInstallation config (zod)
A bundle of tools and inbound/outbound transportsdefineApp + installInstalledInstallation config and account
An external MCP serverdefineMcp + install + mcpAdapterInstalledentry / config; auth in the adapter
Memory (context, entities, episodes, resources)defineMemory + memory fieldThe definition selects itconfigureMemory
Raw access to the Glove agent each rundefineLayer + layers fieldSelectedconfigureLayer
Observe model and tool eventsdefineSubscriber + subscribersSelectedCode
A typed function the model or your run can calldefineCall + callsListedCode
Hooks, skills, subagentshooks, skills, subagentsListedCode
!

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.

agents/support/tools/lookup-order.tool.tstypescript
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.

agents/support/apps/helpdesk.app.tstypescript
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.

agents/support/mcp/docs-search.mcp.tstypescript
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;
agents/support/mcp/kb.mcp.tstypescript
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:

agents/support/mcp/tracker.mcp.tstypescript
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 fieldMeaning
name, descriptionRequired. Shown to glove-mcp's discovery subagent.
urlHTTP endpoint (short form). Use either url or transport.
transport{ kind: "http", url } or { kind: "stdio", command, args?, cwd? }
includeToolsExact server tool names or globs. A non-empty list is an allowlist.
excludeToolsExact names or globs to hide, un-namespaced ("delete_repo", not "kb__delete_repo").
resources, promptsMount list/read resource and list/get prompt utility tools. Both default to true.
connectTimeoutMsBound on connect/initialize. Default 30 s.
requestTimeoutMsBound on every MCP operation. Default 60 s.
idleTimeoutMs, maxLifetimeMsStdio only: close an idle child / recycle after a lifetime; reopened on demand. 0 disables.
tags, metadataOptional 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.

agents/support/agent.ts (excerpt)typescript
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) and resolved ({ definition, installation, config, entry } per installed server).
  • Foundry overrides getActive with 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.
  • getAuthHeaders takes precedence over getAccessToken. Foundry never acquires or refreshes tokens; your adapter does. See glove-mcp.

Put them in the catalogue

agents/support/composition.tstypescript
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:

agents/support/instances.tstypescript
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:

scripts/provision.tstypescript
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));
HTTPhttp
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 + id exist in that definition's components; 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 an accountId in the PUT /api/installations body or in PATCH /api/agent-instances/:id installations. Config may not contain an accountId key.
  • 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):

agents/support/instances.tstypescript
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

agents/support/memory/customer.memory.tstypescript
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;
KeyAdapter returned by adapter(ctx)Mounts
contextContextAdapterAmbient context on the main agent (useContext)
entityEntityMemoryAdapterEntity reader or curator tools
episodicEpisodicMemoryAdapterEpisodic reader or curator tools
resourcesResourceFsAdapterResource 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.

agents/support/layers/tenant.layer.tstypescript
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

agents/support/subscribers/audit.subscriber.tstypescript
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

agents/support/agent.tstypescript
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.

agents/support/agent.ts (excerpt)typescript
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.
  • invoke parses 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

EndpointClientReturns
GET /api/capabilities?definition=supportcapabilities("support"){ tools, applications, mcp, memory }, each entry { id, kind, description, ownership }; ownership is instance for installables, definition for memory
GET /api/surfaces?definition=supportsurfaces("support"){ layers, subscribers } in the catalogue
GET /api/installations?agent=<id>installations(id)The instance's installation records
GET /api/manifestmanifest()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.