- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Skill Sources
Inline skills are fine for a demo, but real skills live somewhere: a folder in your repo, a bundle baked at build time, or rows in a database. A SkillSource is how withSkills reads them. This page covers the three built-in sources and how to combine them.
Every source is bytes only, so the same middleware works on the edge, in a Worker, or on a server. Pick the source that matches where your skills live.
From a folder#
skillDirectory walks a folder for SKILL.md files. It lives under the /node entry point because it reads the filesystem, so use it on a server, not the edge.
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { withSkills } from '@tanstack/ai-skills'
import { skillDirectory } from '@tanstack/ai-skills/node'
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: anthropicText('claude-sonnet-4-5'),
messages,
middleware: [withSkills(skillDirectory('./skills'))],
})
return toServerSentEventsResponse(stream)
}A skill folder is any directory that contains a SKILL.md. Bundled files under references/ and assets/ become the skill's resources, and files under scripts/ are listed too (they are inventoried, not run, in this release).
By default skillDirectory is strict: a malformed SKILL.md is an error, so a broken file never slips into your catalog silently. Pass { strict: false } to skip bad files and load the rest.
From a build-time bundle (edge-safe)#
For an edge deployment you cannot read the filesystem at request time. The Vite plugin globs your skills at build time and bakes them into the bundle, so staticSkills needs no filesystem at runtime.
Add the plugin to your Vite config:
import { defineConfig } from 'vite'
import { skillsCatalogPlugin } from '@tanstack/ai-skills/node'
export default defineConfig({
plugins: [skillsCatalogPlugin({ dir: 'skills' })],
})Then wrap the generated catalog:
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { withSkills } from '@tanstack/ai-skills'
import { staticSkills } from '@tanstack/ai-skills/static'
import { catalog } from 'virtual:tanstack-skills'
const skills = staticSkills(catalog)
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: anthropicText('claude-sonnet-4-5'),
messages,
middleware: [withSkills(skills)],
})
return toServerSentEventsResponse(stream)
}Because the catalog is baked at build time, skills.names is a typed list of your skill names, and the load_skill tool's name is constrained to it.
From your own store#
For DB, S3, or registry-backed skills, define a SkillSource yourself. It is a small interface: list the skills, and load one by name. See Write a skill source for the full walkthrough and the conformance suite that proves your adapter behaves.
Combine sources#
Real setups layer skills: org-wide, per-project, per-tenant. Combine them with the combinators, then pass the result to withSkills.
import { aggregate, dedupe } from '@tanstack/ai-skills'
// Org skills plus this tenant's skills, org first on a name clash.
const source = dedupe(aggregate([orgSkills, tenantSkills]))| Combinator | What it does |
|---|---|
| aggregate([a, b]) | Concatenate sources in order. No dedupe. |
| dedupe(source) | First occurrence of a name wins, warns on a clash. |
| filter(source, fn) | Hide skills your predicate rejects. Hidden skills never reach the catalog. |
| cache(source) | Memoize list() and load(). Shares one fetch across concurrent calls. |
Passing an array straight to withSkills([a, b]) is shorthand for dedupe(aggregate([a, b])). A single source is used as-is and never wrapped, so a tenant-scoped source is never cached into a shared bucket by accident. Reach for cache yourself only when the source is safe to share.
Where to go next#
- Portable Agent Skills — the middleware, the catalog, and the load_skill flow.
- Write a skill source — back skills with your own store.