@girardmedia/bootspring 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: markdown-processing
|
|
3
|
+
description: Markdown processing patterns with remark/rehype, MDX compilation, syntax highlighting, custom plugins, and content pipelines.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Markdown Processing Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use these patterns when building documentation sites, blogs, CMSs, or any application that renders markdown content. The unified ecosystem (remark for markdown, rehype for HTML) provides a plugin pipeline for transforming content. Use MDX when you need interactive React components inside markdown. These patterns cover the full pipeline from raw markdown to styled, interactive HTML.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Remark/Rehype Pipeline
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/markdown/processor.ts
|
|
17
|
+
import { unified } from 'unified';
|
|
18
|
+
import remarkParse from 'remark-parse';
|
|
19
|
+
import remarkGfm from 'remark-gfm';
|
|
20
|
+
import remarkMath from 'remark-math';
|
|
21
|
+
import remarkRehype from 'remark-rehype';
|
|
22
|
+
import rehypeSlug from 'rehype-slug';
|
|
23
|
+
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
24
|
+
import rehypeHighlight from 'rehype-highlight';
|
|
25
|
+
import rehypeKatex from 'rehype-katex';
|
|
26
|
+
import rehypeSanitize from 'rehype-sanitize';
|
|
27
|
+
import rehypeStringify from 'rehype-stringify';
|
|
28
|
+
|
|
29
|
+
export async function processMarkdown(markdown: string): Promise<{
|
|
30
|
+
html: string;
|
|
31
|
+
headings: Array<{ depth: number; text: string; id: string }>;
|
|
32
|
+
}> {
|
|
33
|
+
const headings: Array<{ depth: number; text: string; id: string }> = [];
|
|
34
|
+
|
|
35
|
+
const result = await unified()
|
|
36
|
+
.use(remarkParse)
|
|
37
|
+
.use(remarkGfm) // tables, strikethrough, task lists
|
|
38
|
+
.use(remarkMath) // LaTeX math blocks
|
|
39
|
+
.use(extractHeadings, { headings }) // custom plugin
|
|
40
|
+
.use(remarkRehype, { allowDangerousHtml: false })
|
|
41
|
+
.use(rehypeSlug) // add id to headings
|
|
42
|
+
.use(rehypeAutolinkHeadings, { behavior: 'wrap' })
|
|
43
|
+
.use(rehypeHighlight) // syntax highlighting
|
|
44
|
+
.use(rehypeKatex) // render math
|
|
45
|
+
.use(rehypeSanitize) // XSS protection
|
|
46
|
+
.use(rehypeStringify)
|
|
47
|
+
.process(markdown);
|
|
48
|
+
|
|
49
|
+
return { html: String(result), headings };
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Custom Remark Plugin
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// src/markdown/plugins/extract-headings.ts
|
|
57
|
+
import { visit } from 'unist-util-visit';
|
|
58
|
+
import { toString } from 'mdast-util-to-string';
|
|
59
|
+
import type { Root, Heading } from 'mdast';
|
|
60
|
+
import type { Plugin } from 'unified';
|
|
61
|
+
|
|
62
|
+
interface Options {
|
|
63
|
+
headings: Array<{ depth: number; text: string; id: string }>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const extractHeadings: Plugin<[Options], Root> = (options) => {
|
|
67
|
+
return (tree) => {
|
|
68
|
+
visit(tree, 'heading', (node: Heading) => {
|
|
69
|
+
const text = toString(node);
|
|
70
|
+
const id = text.toLowerCase().replace(/[^\w]+/g, '-').replace(/(^-|-$)/g, '');
|
|
71
|
+
options.headings.push({ depth: node.depth, text, id });
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Reading Time Plugin
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// src/markdown/plugins/reading-time.ts
|
|
81
|
+
import { toString } from 'mdast-util-to-string';
|
|
82
|
+
import type { Root } from 'mdast';
|
|
83
|
+
import type { Plugin } from 'unified';
|
|
84
|
+
import type { VFile } from 'vfile';
|
|
85
|
+
|
|
86
|
+
const WORDS_PER_MINUTE = 200;
|
|
87
|
+
|
|
88
|
+
export const remarkReadingTime: Plugin<[], Root> = () => {
|
|
89
|
+
return (tree: Root, file: VFile) => {
|
|
90
|
+
const text = toString(tree);
|
|
91
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
92
|
+
const minutes = Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));
|
|
93
|
+
|
|
94
|
+
(file.data as any).readingTime = { words, minutes, text: `${minutes} min read` };
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Syntax Highlighting with Shiki
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// src/markdown/highlighter.ts
|
|
103
|
+
import { createHighlighter, type BundledLanguage } from 'shiki';
|
|
104
|
+
|
|
105
|
+
let highlighter: Awaited<ReturnType<typeof createHighlighter>> | null = null;
|
|
106
|
+
|
|
107
|
+
async function getHighlighter() {
|
|
108
|
+
if (!highlighter) {
|
|
109
|
+
highlighter = await createHighlighter({
|
|
110
|
+
themes: ['github-dark', 'github-light'],
|
|
111
|
+
langs: ['typescript', 'javascript', 'jsx', 'tsx', 'python', 'bash', 'json', 'sql', 'css', 'html'],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return highlighter;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function highlightCode(code: string, lang: string, theme: 'dark' | 'light' = 'dark') {
|
|
118
|
+
const h = await getHighlighter();
|
|
119
|
+
return h.codeToHtml(code.trim(), {
|
|
120
|
+
lang: lang as BundledLanguage,
|
|
121
|
+
theme: theme === 'dark' ? 'github-dark' : 'github-light',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Rehype plugin using Shiki
|
|
126
|
+
import { visit } from 'unist-util-visit';
|
|
127
|
+
import type { Element, Root } from 'hast';
|
|
128
|
+
|
|
129
|
+
export function rehypeShiki(): (tree: Root) => Promise<void> {
|
|
130
|
+
return async (tree) => {
|
|
131
|
+
const h = await getHighlighter();
|
|
132
|
+
const promises: Promise<void>[] = [];
|
|
133
|
+
|
|
134
|
+
visit(tree, 'element', (node: Element, _index, parent) => {
|
|
135
|
+
if (node.tagName !== 'code' || (parent as Element)?.tagName !== 'pre') return;
|
|
136
|
+
|
|
137
|
+
const className = (node.properties?.className as string[]) ?? [];
|
|
138
|
+
const lang = className.find((c) => c.startsWith('language-'))?.replace('language-', '') ?? 'text';
|
|
139
|
+
|
|
140
|
+
const code = toString(node);
|
|
141
|
+
promises.push(
|
|
142
|
+
(async () => {
|
|
143
|
+
const html = h.codeToHtml(code, { lang: lang as BundledLanguage, theme: 'github-dark' });
|
|
144
|
+
// Replace the pre>code with Shiki output
|
|
145
|
+
Object.assign(parent!, { type: 'raw', value: html });
|
|
146
|
+
})()
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await Promise.all(promises);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### MDX Compilation
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// src/markdown/mdx.ts
|
|
159
|
+
import { compile, run } from '@mdx-js/mdx';
|
|
160
|
+
import * as runtime from 'react/jsx-runtime';
|
|
161
|
+
import remarkGfm from 'remark-gfm';
|
|
162
|
+
import rehypeSlug from 'rehype-slug';
|
|
163
|
+
|
|
164
|
+
// Custom components available in MDX
|
|
165
|
+
const components = {
|
|
166
|
+
Callout: ({ type, children }: { type: 'info' | 'warning' | 'error'; children: React.ReactNode }) => (
|
|
167
|
+
<div className={`callout callout-${type}`} role="alert">{children}</div>
|
|
168
|
+
),
|
|
169
|
+
CodeTabs: ({ children }: { children: React.ReactNode }) => (
|
|
170
|
+
<div className="code-tabs">{children}</div>
|
|
171
|
+
),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export async function compileMDX(source: string) {
|
|
175
|
+
const code = String(
|
|
176
|
+
await compile(source, {
|
|
177
|
+
outputFormat: 'function-body',
|
|
178
|
+
remarkPlugins: [remarkGfm],
|
|
179
|
+
rehypePlugins: [rehypeSlug],
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const { default: Content } = await run(code, {
|
|
184
|
+
...runtime,
|
|
185
|
+
baseUrl: import.meta.url,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return { Content, components };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Usage in React
|
|
192
|
+
function MDXRenderer({ source }: { source: string }) {
|
|
193
|
+
const [Content, setContent] = useState<React.FC | null>(null);
|
|
194
|
+
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
compileMDX(source).then(({ Content }) => setContent(() => Content));
|
|
197
|
+
}, [source]);
|
|
198
|
+
|
|
199
|
+
if (!Content) return <div>Loading...</div>;
|
|
200
|
+
return <Content components={components} />;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Content Pipeline (Frontmatter + Processing)
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// src/markdown/content-pipeline.ts
|
|
208
|
+
import matter from 'gray-matter';
|
|
209
|
+
import { z } from 'zod';
|
|
210
|
+
|
|
211
|
+
const frontmatterSchema = z.object({
|
|
212
|
+
title: z.string(),
|
|
213
|
+
description: z.string().optional(),
|
|
214
|
+
date: z.coerce.date(),
|
|
215
|
+
tags: z.array(z.string()).default([]),
|
|
216
|
+
draft: z.boolean().default(false),
|
|
217
|
+
author: z.string().optional(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
type Frontmatter = z.infer<typeof frontmatterSchema>;
|
|
221
|
+
|
|
222
|
+
export interface ProcessedContent {
|
|
223
|
+
frontmatter: Frontmatter;
|
|
224
|
+
html: string;
|
|
225
|
+
headings: Array<{ depth: number; text: string; id: string }>;
|
|
226
|
+
readingTime: { words: number; minutes: number; text: string };
|
|
227
|
+
excerpt: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function processContentFile(filepath: string): Promise<ProcessedContent> {
|
|
231
|
+
const raw = await fs.readFile(filepath, 'utf-8');
|
|
232
|
+
const { data, content } = matter(raw);
|
|
233
|
+
|
|
234
|
+
// Validate frontmatter
|
|
235
|
+
const frontmatter = frontmatterSchema.parse(data);
|
|
236
|
+
|
|
237
|
+
// Process markdown
|
|
238
|
+
const { html, headings } = await processMarkdown(content);
|
|
239
|
+
|
|
240
|
+
// Generate excerpt (first paragraph)
|
|
241
|
+
const excerpt = content.split('\n\n')[0].replace(/[#*_`]/g, '').trim().slice(0, 200);
|
|
242
|
+
|
|
243
|
+
return { frontmatter, html, headings, readingTime: { words: 0, minutes: 0, text: '' }, excerpt };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Build a searchable index
|
|
247
|
+
export async function buildContentIndex(contentDir: string) {
|
|
248
|
+
const files = await glob('**/*.md', { cwd: contentDir });
|
|
249
|
+
const index = await Promise.all(
|
|
250
|
+
files.map(async (file) => {
|
|
251
|
+
const content = await processContentFile(path.join(contentDir, file));
|
|
252
|
+
return {
|
|
253
|
+
slug: file.replace(/\.md$/, ''),
|
|
254
|
+
...content.frontmatter,
|
|
255
|
+
excerpt: content.excerpt,
|
|
256
|
+
headings: content.headings,
|
|
257
|
+
};
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return index
|
|
262
|
+
.filter((item) => !item.draft)
|
|
263
|
+
.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Custom Rehype Plugin (Image Optimization)
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// src/markdown/plugins/rehype-optimized-images.ts
|
|
271
|
+
import { visit } from 'unist-util-visit';
|
|
272
|
+
import type { Element, Root } from 'hast';
|
|
273
|
+
|
|
274
|
+
export function rehypeOptimizedImages(options: { basePath: string; widths: number[] }) {
|
|
275
|
+
return (tree: Root) => {
|
|
276
|
+
visit(tree, 'element', (node: Element) => {
|
|
277
|
+
if (node.tagName !== 'img') return;
|
|
278
|
+
|
|
279
|
+
const src = node.properties?.src as string;
|
|
280
|
+
if (!src || src.startsWith('http')) return;
|
|
281
|
+
|
|
282
|
+
const srcset = options.widths
|
|
283
|
+
.map((w) => `${options.basePath}/_next/image?url=${encodeURIComponent(src)}&w=${w} ${w}w`)
|
|
284
|
+
.join(', ');
|
|
285
|
+
|
|
286
|
+
node.properties = {
|
|
287
|
+
...node.properties,
|
|
288
|
+
srcset,
|
|
289
|
+
sizes: '(max-width: 768px) 100vw, 768px',
|
|
290
|
+
loading: 'lazy',
|
|
291
|
+
decoding: 'async',
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Examples
|
|
299
|
+
|
|
300
|
+
| Plugin | Phase | Purpose |
|
|
301
|
+
|--------|-------|---------|
|
|
302
|
+
| `remark-gfm` | Parse | Tables, task lists, autolinks |
|
|
303
|
+
| `remark-math` | Parse | LaTeX math expressions |
|
|
304
|
+
| `remark-rehype` | Transform | Markdown AST to HTML AST |
|
|
305
|
+
| `rehype-slug` | Transform | Add IDs to headings |
|
|
306
|
+
| `rehype-highlight` | Transform | Syntax highlighting |
|
|
307
|
+
| `rehype-sanitize` | Transform | XSS protection |
|
|
308
|
+
| `rehype-stringify` | Stringify | HTML AST to string |
|
|
309
|
+
|
|
310
|
+
## Checklist
|
|
311
|
+
- [ ] Markdown processed through unified pipeline (remark + rehype)
|
|
312
|
+
- [ ] GFM enabled for tables, task lists, and strikethrough
|
|
313
|
+
- [ ] Syntax highlighting uses Shiki or rehype-highlight with language detection
|
|
314
|
+
- [ ] Headings have slugified IDs for anchor links
|
|
315
|
+
- [ ] HTML output sanitized to prevent XSS from user content
|
|
316
|
+
- [ ] Frontmatter validated with Zod schema
|
|
317
|
+
- [ ] Reading time calculated and exposed to templates
|
|
318
|
+
- [ ] Images have lazy loading, srcset, and sizes attributes
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp-server-patterns
|
|
3
|
+
description: Build MCP servers — tool definitions, resource providers, stdio transport, input validation, error handling, and testing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MCP Server Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building Model Context Protocol (MCP) servers that
|
|
11
|
+
expose tools and resources to AI assistants (Claude, Copilot, etc.). MCP
|
|
12
|
+
servers act as a bridge between AI models and external capabilities — file
|
|
13
|
+
systems, APIs, databases, or custom business logic. Use this skill when
|
|
14
|
+
creating tool integrations, resource providers, or extending an existing
|
|
15
|
+
MCP server with new capabilities.
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
### 1. Server Setup — stdio Transport
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
name: 'my-tools',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Start the server on stdio
|
|
32
|
+
const transport = new StdioServerTransport();
|
|
33
|
+
await server.connect(transport);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Tool Definitions — Input Schema and Handler
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Simple tool with validated inputs
|
|
40
|
+
server.tool(
|
|
41
|
+
'search_files',
|
|
42
|
+
'Search for files matching a pattern in the project directory',
|
|
43
|
+
{
|
|
44
|
+
pattern: z.string().describe('Glob pattern to match (e.g., "**/*.ts")'),
|
|
45
|
+
directory: z.string().optional().describe('Starting directory (default: project root)'),
|
|
46
|
+
maxResults: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
|
|
47
|
+
},
|
|
48
|
+
async ({ pattern, directory, maxResults }) => {
|
|
49
|
+
const results = await globSearch(pattern, {
|
|
50
|
+
cwd: directory ?? process.cwd(),
|
|
51
|
+
limit: maxResults,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
content: [{
|
|
56
|
+
type: 'text',
|
|
57
|
+
text: results.length > 0
|
|
58
|
+
? results.map(f => `- ${f}`).join('\n')
|
|
59
|
+
: `No files found matching "${pattern}"`,
|
|
60
|
+
}],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Tool that returns structured data
|
|
66
|
+
server.tool(
|
|
67
|
+
'query_database',
|
|
68
|
+
'Run a read-only SQL query against the project database',
|
|
69
|
+
{
|
|
70
|
+
query: z.string().describe('SQL SELECT query to execute'),
|
|
71
|
+
},
|
|
72
|
+
async ({ query }) => {
|
|
73
|
+
// Validate query is read-only
|
|
74
|
+
const normalized = query.trim().toUpperCase();
|
|
75
|
+
if (!normalized.startsWith('SELECT') && !normalized.startsWith('WITH')) {
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: 'text', text: 'Error: Only SELECT queries are allowed' }],
|
|
78
|
+
isError: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const rows = await db.query(query);
|
|
84
|
+
return {
|
|
85
|
+
content: [{
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: JSON.stringify(rows, null, 2),
|
|
88
|
+
}],
|
|
89
|
+
};
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: `Query error: ${(err as Error).message}` }],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. Resource Providers — Expose Contextual Data
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Static resource
|
|
104
|
+
server.resource(
|
|
105
|
+
'project-config',
|
|
106
|
+
'project://config',
|
|
107
|
+
async (uri) => ({
|
|
108
|
+
contents: [{
|
|
109
|
+
uri: uri.href,
|
|
110
|
+
mimeType: 'application/json',
|
|
111
|
+
text: JSON.stringify(await loadProjectConfig(), null, 2),
|
|
112
|
+
}],
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Dynamic resource with URI template
|
|
117
|
+
server.resource(
|
|
118
|
+
'file-contents',
|
|
119
|
+
new ResourceTemplate('file:///{path}', { list: undefined }),
|
|
120
|
+
async (uri, { path }) => {
|
|
121
|
+
const content = await fs.readFile(path as string, 'utf-8');
|
|
122
|
+
return {
|
|
123
|
+
contents: [{
|
|
124
|
+
uri: uri.href,
|
|
125
|
+
mimeType: getMimeType(path as string),
|
|
126
|
+
text: content,
|
|
127
|
+
}],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 4. Error Handling — Graceful Failure
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
server.tool(
|
|
137
|
+
'deploy_preview',
|
|
138
|
+
'Deploy a preview environment for the current branch',
|
|
139
|
+
{
|
|
140
|
+
branch: z.string().describe('Git branch to deploy'),
|
|
141
|
+
},
|
|
142
|
+
async ({ branch }) => {
|
|
143
|
+
try {
|
|
144
|
+
// Validate branch exists
|
|
145
|
+
const branches = await git.listBranches();
|
|
146
|
+
if (!branches.includes(branch)) {
|
|
147
|
+
return {
|
|
148
|
+
content: [{
|
|
149
|
+
type: 'text',
|
|
150
|
+
text: `Branch "${branch}" not found. Available branches:\n${branches.join('\n')}`,
|
|
151
|
+
}],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = await deployPreview(branch);
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: `Preview deployed successfully.\nURL: ${result.url}\nStatus: ${result.status}`,
|
|
161
|
+
}],
|
|
162
|
+
};
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
+
return {
|
|
166
|
+
content: [{
|
|
167
|
+
type: 'text',
|
|
168
|
+
text: `Deployment failed: ${message}\n\nTroubleshooting:\n- Check that Docker is running\n- Verify cloud credentials are configured`,
|
|
169
|
+
}],
|
|
170
|
+
isError: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 5. Tool Design Principles
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// GOOD: Clear name, specific description, constrained inputs
|
|
181
|
+
server.tool(
|
|
182
|
+
'create_github_issue',
|
|
183
|
+
'Create a new issue in the specified GitHub repository',
|
|
184
|
+
{
|
|
185
|
+
repo: z.string().regex(/^[\w-]+\/[\w-]+$/).describe('Repository (owner/name)'),
|
|
186
|
+
title: z.string().min(1).max(256).describe('Issue title'),
|
|
187
|
+
body: z.string().optional().describe('Issue body in markdown'),
|
|
188
|
+
labels: z.array(z.string()).optional().describe('Labels to apply'),
|
|
189
|
+
},
|
|
190
|
+
handler
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// BAD: Vague name, no input validation, overly broad
|
|
194
|
+
server.tool(
|
|
195
|
+
'do_github_stuff',
|
|
196
|
+
'Does stuff with GitHub',
|
|
197
|
+
{ data: z.any() },
|
|
198
|
+
handler
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Tool design rules:**
|
|
203
|
+
- Name: verb_noun format, lowercase with underscores
|
|
204
|
+
- Description: one sentence explaining what the tool does and when to use it
|
|
205
|
+
- Inputs: use Zod for validation with `.describe()` on every field
|
|
206
|
+
- Outputs: return structured text, not raw JSON dumps
|
|
207
|
+
- Errors: return `isError: true` with actionable messages, not stack traces
|
|
208
|
+
|
|
209
|
+
### 6. Multi-Tool Server Pattern
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// Organize tools by domain in separate files
|
|
213
|
+
// tools/files.ts
|
|
214
|
+
export function registerFileTools(server: McpServer) {
|
|
215
|
+
server.tool('read_file', '...', schema, handler);
|
|
216
|
+
server.tool('write_file', '...', schema, handler);
|
|
217
|
+
server.tool('search_files', '...', schema, handler);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// tools/git.ts
|
|
221
|
+
export function registerGitTools(server: McpServer) {
|
|
222
|
+
server.tool('git_status', '...', schema, handler);
|
|
223
|
+
server.tool('git_diff', '...', schema, handler);
|
|
224
|
+
server.tool('git_log', '...', schema, handler);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// index.ts
|
|
228
|
+
const server = new McpServer({ name: 'dev-tools', version: '1.0.0' });
|
|
229
|
+
registerFileTools(server);
|
|
230
|
+
registerGitTools(server);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 7. Testing MCP Tools
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
237
|
+
|
|
238
|
+
describe('search_files tool', () => {
|
|
239
|
+
it('returns matching files', async () => {
|
|
240
|
+
const result = await searchFilesHandler({
|
|
241
|
+
pattern: '**/*.ts',
|
|
242
|
+
directory: '/tmp/test-project',
|
|
243
|
+
maxResults: 5,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(result.isError).toBeUndefined();
|
|
247
|
+
expect(result.content[0].text).toContain('.ts');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns empty message when no files match', async () => {
|
|
251
|
+
const result = await searchFilesHandler({
|
|
252
|
+
pattern: '**/*.xyz',
|
|
253
|
+
directory: '/tmp/test-project',
|
|
254
|
+
maxResults: 5,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(result.content[0].text).toContain('No files found');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns error for invalid directory', async () => {
|
|
261
|
+
const result = await searchFilesHandler({
|
|
262
|
+
pattern: '**/*.ts',
|
|
263
|
+
directory: '/nonexistent',
|
|
264
|
+
maxResults: 5,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(result.isError).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Examples
|
|
273
|
+
|
|
274
|
+
| Component | Purpose | Protocol Role |
|
|
275
|
+
|-----------|---------|---------------|
|
|
276
|
+
| Tool | Execute actions (read, write, deploy) | `tools/call` |
|
|
277
|
+
| Resource | Expose read-only context (config, schema) | `resources/read` |
|
|
278
|
+
| Prompt | Predefined prompt templates | `prompts/get` |
|
|
279
|
+
| Transport | Communication channel | stdio, SSE, HTTP |
|
|
280
|
+
|
|
281
|
+
## Checklist
|
|
282
|
+
|
|
283
|
+
- [ ] All tools have descriptive names (verb_noun) and one-sentence descriptions
|
|
284
|
+
- [ ] Input schemas use Zod with `.describe()` on every parameter
|
|
285
|
+
- [ ] Tools validate inputs and return `isError: true` on failure
|
|
286
|
+
- [ ] Error messages include troubleshooting hints, not raw exceptions
|
|
287
|
+
- [ ] Read-only operations separated from write operations
|
|
288
|
+
- [ ] Destructive tools require explicit confirmation parameters
|
|
289
|
+
- [ ] Resources expose context data that tools can reference
|
|
290
|
+
- [ ] Tools organized by domain in separate files
|
|
291
|
+
- [ ] Each tool handler has unit tests covering success, error, and edge cases
|
|
292
|
+
- [ ] Server logs to stderr (not stdout) to avoid corrupting stdio transport
|