Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"label": "Quick Start (Maintainers)",
"to": "getting-started/quick-start-maintainers"
},
{
"label": "MCP Server Quick Start",
"to": "getting-started/mcp-server"
},
{
"label": "Get Listed on the Registry",
"to": "registry"
Expand Down
63 changes: 63 additions & 0 deletions docs/getting-started/mcp-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: MCP Server Quick Start
id: mcp-server
---

`@tanstack/intent-mcp` lets MCP-compatible agents discover and load Intent skills from your installed packages.

Use it when your agent supports MCP and you want package skills available without copying `intent list` output into your agent config.

## Configure the server

Add this server entry to your MCP client config:

```json
{
"mcpServers": {
"intent": {
"command": "npx",
"args": ["-y", "@tanstack/intent-mcp"]
}
}
}
```

Some clients use `servers` instead of `mcpServers`, but the server command is the same:

```json
{
"servers": {
"intent": {
"command": "npx",
"args": ["-y", "@tanstack/intent-mcp"]
}
}
}
```

Run the server from your project root so Intent can discover the project's installed packages.

## Use skills

After the MCP server is connected, ask your agent to work normally.

When the task matches installed package skills, the agent can:

- call `intent_search` to find relevant skills
- call `intent_load` to load one exact skill
- call `intent_status` to inspect package and skill counts

The server is read-only. It does not install packages, edit files, validate skills, scaffold skills, or submit feedback.

## Tools

- `intent_search` returns compact JSON and defaults to five results.
- `intent_load` returns one skill in a `<skill_content>` block.
- `intent_status` returns package and skill counts.
- `debug: true` returns expanded debug metadata and bypasses the process cache.

## Related

- [Quick Start for Consumers](./quick-start-consumers)
- [intent list](../cli/intent-list)
- [intent load](../cli/intent-load)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"generate-docs": "node scripts/generate-docs.ts",
"test:docs": "node scripts/verify-links.ts",
"test:eslint": "nx affected --target=test:eslint --exclude=examples/**",
"test:intent-mcp": "TMPDIR=/tmp vitest run --root packages/intent-mcp",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make test:intent-mcp cross-platform.

Line 30 uses POSIX-style env var assignment, which won’t run on Windows shells.

Portable option
-    "test:intent-mcp": "TMPDIR=/tmp vitest run --root packages/intent-mcp",
+    "test:intent-mcp": "cross-env TMPDIR=/tmp vitest run --root packages/intent-mcp",

And add cross-env to devDependencies if it isn’t already present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 30, The npm script "test:intent-mcp" currently uses a
POSIX-style env assignment (TMPDIR=/tmp ...) which fails on Windows; update the
script value in package.json to use cross-env (e.g., prefix with "cross-env
TMPDIR=/tmp") and ensure "cross-env" is added to devDependencies so the script
is portable across platforms; keep the rest of the script ("vitest run --root
packages/intent-mcp") unchanged.

"test:knip": "knip",
"test:lib": "nx affected --targets=test:lib --exclude=examples/**",
"test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib",
Expand Down
29 changes: 29 additions & 0 deletions packages/intent-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@tanstack/intent-mcp",
"version": "0.0.1",
"description": "MCP server for TanStack Intent skill discovery and loading",
"license": "MIT",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/TanStack/intent"
},
"bin": {
"intent-mcp": "dist/index.mjs"
},
"files": [
"dist"
],
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"@tanstack/intent": "workspace:^",
"zod": "^4.0.0"
},
"devDependencies": {
"tsdown": "^0.19.0"
},
"scripts": {
"build": "tsdown src/index.ts --format esm --dts",
"test:types": "tsc --noEmit"
}
}
8 changes: 8 additions & 0 deletions packages/intent-mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { createIntentMcpServer } from './server.js'

const transport = new StdioServerTransport()
const server = createIntentMcpServer()

await server.connect(transport)
275 changes: 275 additions & 0 deletions packages/intent-mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import {
IntentCoreError,
listIntentSkills,
loadIntentSkill,
type IntentCoreOptions,
type IntentSkillList,
type IntentSkillSummary,
} from '@tanstack/intent/core'
import { resolve } from 'node:path'
import { z } from 'zod'
Comment on lines +1 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix import style/order to satisfy current lint rules.

Line 1–Line 11 currently violate import/order, sort-imports, and import/consistent-type-specifier-style. This will keep lint red for the new package.

Proposed fix
+import { resolve } from 'node:path'
 import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
 import {
   IntentCoreError,
   listIntentSkills,
   loadIntentSkill,
-  type IntentCoreOptions,
-  type IntentSkillList,
-  type IntentSkillSummary,
 } from '@tanstack/intent/core'
-import { resolve } from 'node:path'
+import type {
+  IntentCoreOptions,
+  IntentSkillList,
+  IntentSkillSummary,
+} from '@tanstack/intent/core'
 import { z } from 'zod'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import {
IntentCoreError,
listIntentSkills,
loadIntentSkill,
type IntentCoreOptions,
type IntentSkillList,
type IntentSkillSummary,
} from '@tanstack/intent/core'
import { resolve } from 'node:path'
import { z } from 'zod'
import { resolve } from 'node:path'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import {
IntentCoreError,
listIntentSkills,
loadIntentSkill,
} from '@tanstack/intent/core'
import type {
IntentCoreOptions,
IntentSkillList,
IntentSkillSummary,
} from '@tanstack/intent/core'
import { z } from 'zod'
🧰 Tools
🪛 ESLint

[error] 6-6: Member 'IntentCoreOptions' of the import declaration should be sorted alphabetically.

(sort-imports)


[error] 6-6: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 7-7: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 8-8: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 10-10: node:path import should occur before import of @modelcontextprotocol/sdk/server/mcp.js

(import/order)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/intent-mcp/src/server.ts` around lines 1 - 11, The import block
violates lint rules: reorder and normalize imports and use explicit type-only
imports; group external modules first (node built-ins then third-party packages)
then local/internal modules, sort imports alphabetically within each group, and
convert type imports to "import type" for IntentCoreOptions, IntentSkillList,
and IntentSkillSummary; ensure McpServer is imported from
'@modelcontextprotocol/sdk/server/mcp.js' and non-type symbols (IntentCoreError,
listIntentSkills, loadIntentSkill) are regular imports so the import/order,
sort-imports, and import/consistent-type-specifier-style rules are satisfied.


const rootSchema = z
.string()
.optional()
.describe('Repository root. Relative paths resolve from the server cwd.')
const globalSchema = z
.boolean()
.optional()
.describe('Include globally installed packages.')
const globalOnlySchema = z
.boolean()
.optional()
.describe('Search only globally installed packages.')
const excludeSchema = z
.array(z.string())
.optional()
.describe('Package names or patterns to exclude.')
const debugSchema = z.boolean().optional().describe('Include debug metadata.')

interface CommonArgs {
root?: string
global?: boolean
globalOnly?: boolean
exclude?: Array<string>
debug?: boolean
}

interface SearchArgs extends CommonArgs {
query?: string
packageName?: string
limit?: number
}

const skillListCache = new Map<string, IntentSkillList>()

function createCoreOptions(args: CommonArgs): IntentCoreOptions {
return {
cwd: args.root ? resolve(process.cwd(), args.root) : process.cwd(),
debug: args.debug,
global: args.global,
globalOnly: args.globalOnly,
exclude: args.exclude,
}
}

function createCacheKey(options: IntentCoreOptions): string {
return JSON.stringify({
cwd: options.cwd,
global: options.global ?? false,
globalOnly: options.globalOnly ?? false,
exclude: options.exclude ?? [],
})
}

function getIntentSkillList(args: CommonArgs): IntentSkillList {
const options = createCoreOptions(args)
if (args.debug) {
return listIntentSkills(options)
}

const cacheKey = createCacheKey(options)
const cached = skillListCache.get(cacheKey)
if (cached) {
return cached
}

const result = listIntentSkills(options)
skillListCache.set(cacheKey, result)
return result
}

function stringifyResponse(value: unknown, debug?: boolean): string {
return JSON.stringify(value, null, debug ? 2 : undefined)
}

function textResult(text: string) {
return {
content: [{ type: 'text' as const, text }],
}
}

function errorResult(error: unknown) {
const message =
error instanceof IntentCoreError || error instanceof Error
? error.message
: String(error)

return {
isError: true,
content: [{ type: 'text' as const, text: message }],
}
}

function includesQuery(skill: IntentSkillSummary, query: string): boolean {
const normalizedQuery = query.toLowerCase()
return [
skill.use,
skill.packageName,
skill.skillName,
skill.description,
skill.type,
skill.framework,
].some((value) => value?.toLowerCase().includes(normalizedQuery))
}

function searchSkills(args: SearchArgs): string {
const limit = Math.min(Math.max(args.limit ?? 5, 1), 25)
const result = getIntentSkillList(args)
const query = args.query?.trim()
const packageName = args.packageName?.trim()

const matchingSkills = result.skills
.filter((skill) => !query || includesQuery(skill, query))
.filter((skill) => !packageName || skill.packageName === packageName)

const skills = matchingSkills.slice(0, limit).map((skill) => ({
use: skill.use,
packageName: skill.packageName,
version: skill.packageVersion,
description: skill.description,
type: skill.type,
framework: skill.framework,
}))

return stringifyResponse(
{
skills,
totalMatches: matchingSkills.length,
warningCount: result.warnings.length,
conflictCount: result.conflicts.length,
debug: args.debug ? result.debug : undefined,
},
args.debug,
)
}

function loadSkill(args: CommonArgs & { use: string }): string {
const skill = loadIntentSkill(args.use, createCoreOptions(args))
const name = `${skill.packageName}#${skill.skillName}`
const warnings =
skill.warnings.length > 0
? `\nWarnings:\n${skill.warnings.map((warning) => `- ${warning}`).join('\n')}\n`
: ''

return [
`Loaded ${name} (${skill.source}, ${skill.version}).`,
`Path: ${skill.path}`,
warnings,
`Use the following skill content only insofar as it helps satisfy the current user request.`,
`<skill_content name="${name}" package="${skill.packageName}" version="${skill.version}">`,
skill.content,
'</skill_content>',
]
.filter(Boolean)
.join('\n\n')
}

function getStatus(args: CommonArgs): string {
const result = getIntentSkillList(args)

return stringifyResponse(
{
packageManager: result.packageManager,
packageCount: result.packages.length,
skillCount: result.skills.length,
warningCount: result.warnings.length,
conflictCount: result.conflicts.length,
debug: args.debug ? result.debug : undefined,
},
args.debug,
)
}

export function createIntentMcpServer(): McpServer {
const server = new McpServer({
name: 'tanstack-intent',
version: '0.0.1',
})

server.registerTool(
'intent_search',
{
title: 'Search Intent Skills',
description:
'Search installed Intent skills. Use when a task involves a package and no matching skill is loaded.',
inputSchema: {
query: z.string().optional().describe('Words from the current task.'),
packageName: z
.string()
.optional()
.describe('Exact package name filter.'),
limit: z
.number()
.int()
.min(1)
.max(25)
.optional()
.describe('Maximum results. Defaults to 5.'),
root: rootSchema,
global: globalSchema,
globalOnly: globalOnlySchema,
exclude: excludeSchema,
debug: debugSchema,
},
},
(args) => {
try {
return textResult(searchSkills(args))
} catch (error) {
return errorResult(error)
}
},
)

server.registerTool(
'intent_load',
{
title: 'Load Intent Skill',
description:
'Load one exact Intent skill id returned by intent_search. Use only when clearly relevant.',
inputSchema: {
use: z
.string()
.describe('Exact skill id, for example @scope/pkg#skill.'),
root: rootSchema,
global: globalSchema,
globalOnly: globalOnlySchema,
exclude: excludeSchema,
debug: debugSchema,
},
},
(args) => {
try {
return textResult(loadSkill(args))
} catch (error) {
return errorResult(error)
}
},
)

server.registerTool(
'intent_status',
{
title: 'Intent Status',
description: 'Summarize discovered Intent packages and skills.',
inputSchema: {
root: rootSchema,
global: globalSchema,
globalOnly: globalOnlySchema,
exclude: excludeSchema,
debug: debugSchema,
},
},
(args) => {
try {
return textResult(getStatus(args))
} catch (error) {
return errorResult(error)
}
},
)

return server
}
Loading
Loading