Skip to content

Commit 5ac8b34

Browse files
committed
mcp
1 parent 5d77284 commit 5ac8b34

File tree

13 files changed

+1513
-470
lines changed

13 files changed

+1513
-470
lines changed

docs/mcp/overview.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,26 @@ See [Connecting](./connecting) for client-specific setup instructions.
3232

3333
## Available Tools
3434

35-
The MCP server exposes three tools:
36-
37-
| Tool | Description |
38-
| ---------------- | ------------------------------------------------------------ |
39-
| `list_libraries` | List all TanStack libraries with their versions and metadata |
40-
| `get_doc` | Fetch a specific documentation page by library and path |
41-
| `search_docs` | Full-text search across all TanStack documentation |
35+
The MCP server exposes tools for documentation and showcase management:
36+
37+
### Documentation Tools
38+
39+
| Tool | Description | Auth Required |
40+
| ---------------- | ------------------------------------------------------------ | ------------- |
41+
| `list_libraries` | List all TanStack libraries with their versions and metadata | No |
42+
| `get_doc` | Fetch a specific documentation page by library and path | No |
43+
| `search_docs` | Full-text search across all TanStack documentation | No |
44+
45+
### Showcase Tools
46+
47+
| Tool | Description | Auth Required |
48+
| ------------------- | ------------------------------------------ | ------------- |
49+
| `search_showcases` | Search approved TanStack showcase projects | No |
50+
| `get_showcase` | Get details of a specific showcase project | No |
51+
| `submit_showcase` | Submit a new project to the showcase | Yes |
52+
| `update_showcase` | Update your existing showcase submission | Yes |
53+
| `delete_showcase` | Delete your showcase submission | Yes |
54+
| `list_my_showcases` | List your own showcase submissions | Yes |
4255

4356
See [Available Tools](./tools) for detailed parameter documentation.
4457

docs/mcp/tools.md

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ id: tools
33
title: Available Tools
44
---
55

6-
The TanStack MCP Server exposes three tools for accessing documentation. Each tool is designed for a specific use case.
6+
The TanStack MCP Server exposes tools for accessing documentation and managing showcase submissions.
7+
8+
## Documentation Tools
79

810
## list_libraries
911

@@ -141,3 +143,201 @@ Search results include URLs that reveal the documentation path structure. For ex
141143

142144
- Library: `query`
143145
- Path: `framework/react/guides/queries`
146+
147+
---
148+
149+
## Showcase Tools
150+
151+
These tools allow you to interact with the TanStack showcase, a gallery of projects built with TanStack libraries.
152+
153+
### search_showcases
154+
155+
Search approved showcase projects. No authentication required.
156+
157+
#### Parameters
158+
159+
| Parameter | Type | Required | Description |
160+
| --------------- | -------- | -------- | ------------------------------------------------------------ |
161+
| `query` | string | No | Text search across name, tagline, description, and URL |
162+
| `libraryIds` | string[] | No | Filter by TanStack library IDs (e.g., `["query", "router"]`) |
163+
| `useCases` | string[] | No | Filter by use cases (e.g., `["saas", "dashboard"]`) |
164+
| `hasSourceCode` | boolean | No | Filter to only open source projects |
165+
| `featured` | boolean | No | Filter to only featured projects |
166+
| `limit` | number | No | Max results (default: 20, max: 100) |
167+
| `offset` | number | No | Pagination offset (default: 0) |
168+
169+
#### Valid Library IDs
170+
171+
`query`, `router`, `start`, `table`, `form`, `virtual`, `ranger`, `store`, `pacer`, `db`, `ai`, `config`, `devtools`
172+
173+
#### Valid Use Cases
174+
175+
`blog`, `e-commerce`, `saas`, `dashboard`, `documentation`, `portfolio`, `social`, `developer-tool`, `marketing`, `media`
176+
177+
#### Example
178+
179+
```json
180+
{
181+
"name": "search_showcases",
182+
"arguments": {
183+
"libraryIds": ["query", "router"],
184+
"useCases": ["saas"],
185+
"limit": 10
186+
}
187+
}
188+
```
189+
190+
---
191+
192+
### get_showcase
193+
194+
Get details of a specific showcase project by ID.
195+
196+
#### Parameters
197+
198+
| Parameter | Type | Required | Description |
199+
| --------- | ------ | -------- | ------------- |
200+
| `id` | string | Yes | Showcase UUID |
201+
202+
#### Example
203+
204+
```json
205+
{
206+
"name": "get_showcase",
207+
"arguments": {
208+
"id": "550e8400-e29b-41d4-a716-446655440000"
209+
}
210+
}
211+
```
212+
213+
---
214+
215+
### submit_showcase
216+
217+
Submit a new project to the TanStack showcase. **Requires authentication.** Submissions are reviewed by moderators before appearing publicly.
218+
219+
#### Parameters
220+
221+
| Parameter | Type | Required | Description |
222+
| --------------- | -------- | -------- | -------------------------------------- |
223+
| `name` | string | Yes | Project name (max 255 characters) |
224+
| `tagline` | string | Yes | Short description (max 500 characters) |
225+
| `description` | string | No | Full description |
226+
| `url` | string | Yes | Project URL |
227+
| `screenshotUrl` | string | Yes | Screenshot URL |
228+
| `sourceUrl` | string | No | Source code URL (GitHub, etc.) |
229+
| `logoUrl` | string | No | Logo URL |
230+
| `libraries` | string[] | Yes | TanStack library IDs used |
231+
| `useCases` | string[] | Yes | Use case categories |
232+
233+
#### Example
234+
235+
```json
236+
{
237+
"name": "submit_showcase",
238+
"arguments": {
239+
"name": "My Awesome App",
240+
"tagline": "A dashboard built with TanStack Query and Router",
241+
"url": "https://myapp.com",
242+
"screenshotUrl": "https://myapp.com/screenshot.png",
243+
"sourceUrl": "https://github.com/user/myapp",
244+
"libraries": ["query", "router"],
245+
"useCases": ["dashboard", "saas"]
246+
}
247+
}
248+
```
249+
250+
---
251+
252+
### update_showcase
253+
254+
Update an existing showcase submission. **Requires authentication and ownership.** Updates reset the showcase to pending review.
255+
256+
#### Parameters
257+
258+
| Parameter | Type | Required | Description |
259+
| --------------- | -------- | -------- | -------------------------------- |
260+
| `id` | string | Yes | Showcase UUID to update |
261+
| `name` | string | Yes | Project name |
262+
| `tagline` | string | Yes | Short description |
263+
| `description` | string | No | Full description |
264+
| `url` | string | Yes | Project URL |
265+
| `screenshotUrl` | string | Yes | Screenshot URL |
266+
| `sourceUrl` | string | No | Source code URL (null to remove) |
267+
| `logoUrl` | string | No | Logo URL (null to remove) |
268+
| `libraries` | string[] | Yes | TanStack library IDs |
269+
| `useCases` | string[] | Yes | Use case categories |
270+
271+
#### Example
272+
273+
```json
274+
{
275+
"name": "update_showcase",
276+
"arguments": {
277+
"id": "550e8400-e29b-41d4-a716-446655440000",
278+
"name": "My Updated App",
279+
"tagline": "Now with TanStack Form!",
280+
"url": "https://myapp.com",
281+
"screenshotUrl": "https://myapp.com/new-screenshot.png",
282+
"libraries": ["query", "router", "form"],
283+
"useCases": ["dashboard", "saas"]
284+
}
285+
}
286+
```
287+
288+
---
289+
290+
### delete_showcase
291+
292+
Delete a showcase submission. **Requires authentication and ownership.**
293+
294+
#### Parameters
295+
296+
| Parameter | Type | Required | Description |
297+
| --------- | ------ | -------- | ----------------------- |
298+
| `id` | string | Yes | Showcase UUID to delete |
299+
300+
#### Example
301+
302+
```json
303+
{
304+
"name": "delete_showcase",
305+
"arguments": {
306+
"id": "550e8400-e29b-41d4-a716-446655440000"
307+
}
308+
}
309+
```
310+
311+
---
312+
313+
### list_my_showcases
314+
315+
List your own showcase submissions. **Requires authentication.** Returns all your submissions including pending and denied ones.
316+
317+
#### Parameters
318+
319+
| Parameter | Type | Required | Description |
320+
| --------- | ------ | -------- | ------------------------------------------------- |
321+
| `status` | string | No | Filter by status: `pending`, `approved`, `denied` |
322+
| `limit` | number | No | Max results (default: 20, max: 100) |
323+
| `offset` | number | No | Pagination offset (default: 0) |
324+
325+
#### Example
326+
327+
```json
328+
{
329+
"name": "list_my_showcases",
330+
"arguments": {
331+
"status": "pending",
332+
"limit": 10
333+
}
334+
}
335+
```
336+
337+
---
338+
339+
## Rate Limits
340+
341+
- **Read operations** (documentation, search): 60 requests per minute
342+
- **Write operations** (submit, update, delete): 10 requests per hour
343+
- **Pending submission limit**: Maximum 5 pending submissions per user

src/mcp/auth.server.ts

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { mcpApiKeys, mcpRateLimits, users } from '~/db/schema'
33
import { eq, sql } from 'drizzle-orm'
44

55
export type AuthResult =
6-
| { success: true; keyId: string; rateLimitPerMinute: number }
6+
| { success: true; keyId: string; userId: string; rateLimitPerMinute: number }
77
| { success: false; error: string; status: number }
88

99
/**
@@ -90,6 +90,14 @@ export async function validateApiKey(
9090
}
9191
}
9292

93+
if (!apiKey.userId) {
94+
return {
95+
success: false,
96+
error: 'API key has no associated user',
97+
status: 401,
98+
}
99+
}
100+
93101
if (!apiKey.isActive) {
94102
return {
95103
success: false,
@@ -106,9 +114,9 @@ export async function validateApiKey(
106114
}
107115
}
108116

109-
// Check that user has 'mcp' capability
117+
// Check that user has 'mcp' capability or is admin
110118
const capabilities = apiKey.userCapabilities ?? []
111-
if (!capabilities.includes('mcp')) {
119+
if (!capabilities.includes('mcp') && !capabilities.includes('admin')) {
112120
return {
113121
success: false,
114122
error: 'User does not have MCP access',
@@ -125,6 +133,7 @@ export async function validateApiKey(
125133
return {
126134
success: true,
127135
keyId: apiKey.id,
136+
userId: apiKey.userId,
128137
rateLimitPerMinute: apiKey.rateLimitPerMinute,
129138
}
130139
}
@@ -200,12 +209,57 @@ export function getClientIp(request: Request): string {
200209
}
201210

202211
/**
203-
* Clean up old rate limit records (older than 5 minutes)
212+
* Check write rate limit for a user (10 writes per hour by default)
213+
* Used for MCP write operations like submitting/updating showcases
214+
*/
215+
export async function checkWriteRateLimit(
216+
userId: string,
217+
limitPerHour: number = 10,
218+
): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
219+
const now = new Date()
220+
// Round down to the current hour
221+
const windowStart = new Date(
222+
now.getFullYear(),
223+
now.getMonth(),
224+
now.getDate(),
225+
now.getHours(),
226+
0,
227+
0,
228+
0,
229+
)
230+
const resetAt = new Date(windowStart.getTime() + 60 * 60 * 1000) // 1 hour
231+
232+
// Try to increment existing record, or insert new one
233+
const result = await db
234+
.insert(mcpRateLimits)
235+
.values({
236+
identifier: userId,
237+
identifierType: 'user_write',
238+
windowStart,
239+
requestCount: 1,
240+
})
241+
.onConflictDoUpdate({
242+
target: [mcpRateLimits.identifier, mcpRateLimits.windowStart],
243+
set: {
244+
requestCount: sql`${mcpRateLimits.requestCount} + 1`,
245+
},
246+
})
247+
.returning({ requestCount: mcpRateLimits.requestCount })
248+
249+
const currentCount = result[0]?.requestCount ?? 1
250+
const remaining = Math.max(0, limitPerHour - currentCount)
251+
const allowed = currentCount <= limitPerHour
252+
253+
return { allowed, remaining, resetAt }
254+
}
255+
256+
/**
257+
* Clean up old rate limit records (older than 2 hours for hourly limits)
204258
* Call periodically to prevent table bloat
205259
*/
206260
export async function cleanupRateLimits(): Promise<void> {
207-
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000)
261+
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000)
208262
await db
209263
.delete(mcpRateLimits)
210-
.where(sql`${mcpRateLimits.windowStart} < ${fiveMinutesAgo}`)
264+
.where(sql`${mcpRateLimits.windowStart} < ${twoHoursAgo}`)
211265
}

0 commit comments

Comments
 (0)