From 3a2c0cefb09322c80817f00fd4a6efa670d07c51 Mon Sep 17 00:00:00 2001 From: bryantgillespie Date: Thu, 21 May 2026 18:59:10 -0400 Subject: [PATCH 1/4] docs(mcp): clarify MCP overview title --- content/guides/11.ai/2.mcp/0.index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/content/guides/11.ai/2.mcp/0.index.md b/content/guides/11.ai/2.mcp/0.index.md index 717e03e6..62acb59d 100644 --- a/content/guides/11.ai/2.mcp/0.index.md +++ b/content/guides/11.ai/2.mcp/0.index.md @@ -1,7 +1,9 @@ --- stableId: 91f4d2fc-286d-47c0-8942-68af505ce679 -title: Overview +title: Directus MCP description: Connect AI assistants directly to your Directus instance. Let Claude, ChatGPT, and other AI tools manage your content without manual copy-pasting. +navigation: + title: Overview --- `; + } + return inlinePartials(partial, partials, depth + 1); + }); +} + +function stripBlockFences(body: string): string { + const lines = body.split('\n'); + const out: string[] = []; + const openFences: number[] = []; + let inCodeBlock = false; + let codeFence = ''; + + for (const line of lines) { + const codeMatch = line.match(/^[\t ]*(```+|~~~+)/); + if (codeMatch) { + if (!inCodeBlock) { + inCodeBlock = true; + codeFence = codeMatch[1] ?? ''; + } + else if (line.trim().startsWith(codeFence)) { + inCodeBlock = false; + codeFence = ''; + } + out.push(line); + continue; + } + + if (inCodeBlock) { + out.push(line); + continue; + } + + const open = line.match(/^[\t ]*(:{2,})([a-z][a-z0-9-]*)(?:\{[^}]*\})?[\t ]*$/); + if (open) { + const openToken = open[1]; + if (openToken) openFences.push(openToken.length); + continue; + } + + const closeMatch = line.match(/^[\t ]*(:{2,})[\t ]*$/); + if (closeMatch && openFences.length > 0) { + const closeToken = closeMatch[1]; + if (!closeToken) continue; + const closeLen = closeToken.length; + const lastOpen = openFences[openFences.length - 1]; + if (closeLen === lastOpen) { + openFences.pop(); + continue; + } + } + + out.push(line); + } + + return out.join('\n'); +} + +function rewriteInlineDirectives(body: string): string { + return body + .replace(VIDEO_EMBED, (_m, id: string) => `[Watch video](https://www.youtube.com/watch?v=${id})`) + .replace(DOC_CLI_SNIPPET, (_m, cmd: string) => `\n\`\`\`bash\n${cmd}\n\`\`\`\n`) + .replace(CTA_CLOUD_LINE, '') + .replace(PRODUCT_LINK, ''); +} diff --git a/server/utils/sliceUtf8.ts b/server/utils/sliceUtf8.ts new file mode 100644 index 00000000..ad9e3d92 --- /dev/null +++ b/server/utils/sliceUtf8.ts @@ -0,0 +1,13 @@ +export function sliceUtf8(text: string, offset: number, bytes: number): { content: string; nextOffset: number | null; truncated: boolean } { + const buffer = Buffer.from(text, 'utf8'); + const start = Math.min(offset, buffer.length); + let end = Math.min(start + bytes, buffer.length); + let content = buffer.subarray(start, end).toString('utf8'); + while (end < buffer.length && content.endsWith('\uFFFD') && end > start) { + end--; + content = buffer.subarray(start, end).toString('utf8'); + } + const consumed = Buffer.byteLength(content, 'utf8'); + const nextOffset = start + consumed < buffer.length ? start + consumed : null; + return { content, nextOffset, truncated: nextOffset !== null }; +} diff --git a/tests/server/get-directus-file.test.ts b/tests/server/get-directus-file.test.ts new file mode 100644 index 00000000..76074535 --- /dev/null +++ b/tests/server/get-directus-file.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { sliceUtf8 } from '../../server/utils/sliceUtf8'; + +describe('sliceUtf8', () => { + it('backs up to a valid UTF-8 boundary', () => { + const first = sliceUtf8('abc😀def', 0, 5); + + expect(first.content).toBe('abc'); + expect(first.nextOffset).toBe(3); + expect(first.truncated).toBe(true); + + const second = sliceUtf8('abc😀def', first.nextOffset!, 1024); + + expect(second.content).toBe('😀def'); + expect(second.nextOffset).toBeNull(); + expect(second.truncated).toBe(false); + }); +}); From 36f2f49a681276ae2a5907b1b03058433193982f Mon Sep 17 00:00:00 2001 From: Lindsey Zylstra Date: Thu, 28 May 2026 15:12:56 -0700 Subject: [PATCH 4/4] Fixes --- server/mcp/tools/list-docs.ts | 2 +- server/mcp/tools/search-directus-code.ts | 2 +- server/utils/mcp-rate-limit.ts | 9 +++++++++ .../{get-directus-file.test.ts => sliceUtf8.test.ts} | 0 4 files changed, 11 insertions(+), 2 deletions(-) rename tests/server/{get-directus-file.test.ts => sliceUtf8.test.ts} (100%) diff --git a/server/mcp/tools/list-docs.ts b/server/mcp/tools/list-docs.ts index ec1b5e68..9530f37f 100644 --- a/server/mcp/tools/list-docs.ts +++ b/server/mcp/tools/list-docs.ts @@ -14,7 +14,7 @@ export default defineMcpTool({ name: 'list-docs', title: 'List Directus docs', description: - 'List documentation pages with title, path, description, and section. Use this to discover available docs when you do not know exact paths.', + 'List documentation pages with title, path, description, and URL. Use this to discover available docs when you do not know exact paths.', inputSchema: { pathPrefix: z .string() diff --git a/server/mcp/tools/search-directus-code.ts b/server/mcp/tools/search-directus-code.ts index 8a0fd927..130bb9f2 100644 --- a/server/mcp/tools/search-directus-code.ts +++ b/server/mcp/tools/search-directus-code.ts @@ -49,7 +49,7 @@ export default defineMcpTool({ repo: z .enum(DIRECTUS_REPO_SLUGS) .default('directus') - .describe('Allowlisted Directus repo. Defaults to `directus` (core). Use `sdk` for `@directus/sdk`, `extensions-sdk` for `@directus/extensions-sdk`, etc.'), + .describe('Allowlisted Directus repo. Defaults to `directus` (core). Note: `@directus/sdk` and `@directus/extensions-sdk` live inside the `directus` monorepo — use `path:packages/sdk` or `path:packages/extensions-sdk` as a path filter within that repo.'), language: z .string() .optional() diff --git a/server/utils/mcp-rate-limit.ts b/server/utils/mcp-rate-limit.ts index 4f456ce2..794b30f2 100644 --- a/server/utils/mcp-rate-limit.ts +++ b/server/utils/mcp-rate-limit.ts @@ -1,7 +1,16 @@ const buckets = new Map(); +let lastSweep = 0; export function checkMcpRateLimit(key: string, max: number, windowMs: number): { ok: boolean; retryAfter?: number } { const now = Date.now(); + + if (now - lastSweep > windowMs) { + for (const [k, b] of buckets) { + if (b.resetAt < now) buckets.delete(k); + } + lastSweep = now; + } + const bucket = buckets.get(key); if (!bucket || bucket.resetAt <= now) { buckets.set(key, { count: 1, resetAt: now + windowMs }); diff --git a/tests/server/get-directus-file.test.ts b/tests/server/sliceUtf8.test.ts similarity index 100% rename from tests/server/get-directus-file.test.ts rename to tests/server/sliceUtf8.test.ts