mkdnsite 0.0.1 → 1.0.1
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/package.json +8 -3
- package/src/adapters/cloudflare.ts +202 -15
- package/src/adapters/local.ts +38 -17
- package/src/analytics/classify.ts +65 -0
- package/src/analytics/console.ts +39 -0
- package/src/analytics/noop.ts +15 -0
- package/src/analytics/types.ts +49 -0
- package/src/cache/kv.ts +81 -0
- package/src/cache/memory.ts +46 -0
- package/src/cache/response.ts +24 -0
- package/src/cli.ts +301 -51
- package/src/client/scripts.ts +379 -3
- package/src/config/defaults.ts +66 -5
- package/src/config/schema.ts +200 -2
- package/src/content/assets.ts +202 -0
- package/src/content/cache.ts +232 -0
- package/src/content/filesystem.ts +17 -1
- package/src/content/github.ts +169 -102
- package/src/content/nav-builder.ts +120 -0
- package/src/content/r2.ts +214 -0
- package/src/handler.ts +341 -21
- package/src/index.ts +49 -1
- package/src/mcp/server.ts +164 -0
- package/src/mcp/stdio.ts +29 -0
- package/src/mcp/transport.ts +29 -0
- package/src/negotiate/headers.ts +37 -9
- package/src/render/page-shell.ts +249 -8
- package/src/search/index.ts +342 -0
- package/src/security/csp.ts +92 -0
- package/src/theme/{prose-css.ts → base-css.ts} +251 -11
- package/src/theme/build-css.ts +74 -0
package/src/index.ts
CHANGED
|
@@ -13,6 +13,12 @@ export type { HandlerOptions } from './handler.ts'
|
|
|
13
13
|
export { resolveConfig, DEFAULT_CONFIG } from './config/defaults.ts'
|
|
14
14
|
export type {
|
|
15
15
|
MkdnSiteConfig,
|
|
16
|
+
SiteConfig,
|
|
17
|
+
OgConfig,
|
|
18
|
+
AnalyticsConfig,
|
|
19
|
+
CspConfig,
|
|
20
|
+
CacheConfig,
|
|
21
|
+
FaviconConfig,
|
|
16
22
|
ThemeConfig,
|
|
17
23
|
NegotiationConfig,
|
|
18
24
|
ClientConfig,
|
|
@@ -22,12 +28,30 @@ export type {
|
|
|
22
28
|
LinkProps,
|
|
23
29
|
ImageProps,
|
|
24
30
|
CodeBlockProps,
|
|
25
|
-
InlineCodeProps
|
|
31
|
+
InlineCodeProps,
|
|
32
|
+
ColorTokens,
|
|
33
|
+
FontTokens,
|
|
34
|
+
LogoConfig
|
|
26
35
|
} from './config/schema.ts'
|
|
27
36
|
|
|
37
|
+
// Theme utilities
|
|
38
|
+
export { buildThemeCss } from './theme/build-css.ts'
|
|
39
|
+
export { BASE_THEME_CSS } from './theme/base-css.ts'
|
|
40
|
+
|
|
28
41
|
// Content sources
|
|
29
42
|
export { FilesystemSource } from './content/filesystem.ts'
|
|
30
43
|
export { GitHubSource } from './content/github.ts'
|
|
44
|
+
export { R2ContentSource } from './content/r2.ts'
|
|
45
|
+
export type { R2ContentSourceConfig } from './content/r2.ts'
|
|
46
|
+
export { AssetsSource } from './content/assets.ts'
|
|
47
|
+
export type { AssetsSourceConfig } from './content/assets.ts'
|
|
48
|
+
export { buildNavTree } from './content/nav-builder.ts'
|
|
49
|
+
export type { ContentCache } from './content/cache.ts'
|
|
50
|
+
export { MemoryContentCache, KVContentCache } from './content/cache.ts'
|
|
51
|
+
export type { ResponseCache, CachedResponse } from './cache/response.ts'
|
|
52
|
+
export { MemoryResponseCache } from './cache/memory.ts'
|
|
53
|
+
export { KVResponseCache } from './cache/kv.ts'
|
|
54
|
+
export type { FileEntry } from './content/nav-builder.ts'
|
|
31
55
|
export type {
|
|
32
56
|
ContentSource,
|
|
33
57
|
ContentPage,
|
|
@@ -55,3 +79,27 @@ export { generateLlmsTxt } from './discovery/llmstxt.ts'
|
|
|
55
79
|
export type { DeploymentAdapter } from './adapters/types.ts'
|
|
56
80
|
export { detectRuntime } from './adapters/types.ts'
|
|
57
81
|
export { LocalAdapter } from './adapters/local.ts'
|
|
82
|
+
export { CloudflareAdapter } from './adapters/cloudflare.ts'
|
|
83
|
+
export type { CloudflareEnv } from './adapters/cloudflare.ts'
|
|
84
|
+
|
|
85
|
+
// Search
|
|
86
|
+
export { createSearchIndex } from './search/index.ts'
|
|
87
|
+
export type { SearchIndex, SearchResult, SerializedSearchIndex } from './search/index.ts'
|
|
88
|
+
|
|
89
|
+
// MCP
|
|
90
|
+
export { createMcpServer } from './mcp/server.ts'
|
|
91
|
+
export { createMcpHandler } from './mcp/transport.ts'
|
|
92
|
+
export type { McpConfig } from './config/schema.ts'
|
|
93
|
+
|
|
94
|
+
// Traffic analytics
|
|
95
|
+
export type {
|
|
96
|
+
TrafficAnalytics,
|
|
97
|
+
TrafficEvent,
|
|
98
|
+
TrafficType,
|
|
99
|
+
AnalyticsResponseFormat
|
|
100
|
+
} from './analytics/types.ts'
|
|
101
|
+
export { classifyTraffic, BOT_PATTERNS } from './analytics/classify.ts'
|
|
102
|
+
export { NoopAnalytics } from './analytics/noop.ts'
|
|
103
|
+
export { ConsoleAnalytics } from './analytics/console.ts'
|
|
104
|
+
export { WorkersAnalyticsEngineAnalytics } from './adapters/cloudflare.ts'
|
|
105
|
+
export type { TrafficAnalyticsConfig } from './config/schema.ts'
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { ContentSource } from '../content/types.ts'
|
|
4
|
+
import type { SearchIndex } from '../search/index.ts'
|
|
5
|
+
|
|
6
|
+
export function createMcpServer (opts: {
|
|
7
|
+
source: ContentSource
|
|
8
|
+
searchIndex: SearchIndex
|
|
9
|
+
}): McpServer {
|
|
10
|
+
const { source, searchIndex } = opts
|
|
11
|
+
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: 'mkdnsite',
|
|
14
|
+
version: '0.1.0'
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// ─── Tool: search_docs ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
server.tool(
|
|
20
|
+
'search_docs',
|
|
21
|
+
'Full-text search across all documentation pages',
|
|
22
|
+
{
|
|
23
|
+
query: z.string().describe('Search query'),
|
|
24
|
+
limit: z.number().min(1).max(50).optional().describe('Max results (default 10, max 50)')
|
|
25
|
+
},
|
|
26
|
+
async ({ query, limit }) => {
|
|
27
|
+
const results = searchIndex.search(query, limit ?? 10)
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: 'text' as const,
|
|
31
|
+
text: JSON.stringify(results.map(r => ({
|
|
32
|
+
title: r.title,
|
|
33
|
+
slug: r.slug,
|
|
34
|
+
description: r.description,
|
|
35
|
+
excerpt: r.excerpt,
|
|
36
|
+
score: Math.round(r.score * 1000) / 1000
|
|
37
|
+
})), null, 2)
|
|
38
|
+
}]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// ─── Tool: get_page ─────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
server.tool(
|
|
46
|
+
'get_page',
|
|
47
|
+
'Retrieve the full markdown content of a documentation page by slug',
|
|
48
|
+
{
|
|
49
|
+
slug: z.string().describe('Page slug (e.g. /docs/getting-started)')
|
|
50
|
+
},
|
|
51
|
+
async ({ slug }) => {
|
|
52
|
+
const page = await source.getPage(slug)
|
|
53
|
+
if (page == null) {
|
|
54
|
+
return {
|
|
55
|
+
isError: true,
|
|
56
|
+
content: [{
|
|
57
|
+
type: 'text' as const,
|
|
58
|
+
text: `Page not found: ${slug}`
|
|
59
|
+
}]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines: string[] = []
|
|
64
|
+
if (page.meta.title != null) lines.push(`# ${String(page.meta.title)}`)
|
|
65
|
+
if (page.meta.description != null) lines.push(`> ${String(page.meta.description)}`)
|
|
66
|
+
if (lines.length > 0) lines.push('')
|
|
67
|
+
lines.push(page.body)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: [{
|
|
71
|
+
type: 'text' as const,
|
|
72
|
+
text: lines.join('\n')
|
|
73
|
+
}]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// ─── Tool: list_pages ───────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
server.tool(
|
|
81
|
+
'list_pages',
|
|
82
|
+
'List all available documentation pages',
|
|
83
|
+
{},
|
|
84
|
+
async () => {
|
|
85
|
+
const pages = await source.listPages()
|
|
86
|
+
const list = pages.map(p => ({
|
|
87
|
+
title: String(p.meta.title ?? p.slug),
|
|
88
|
+
slug: p.slug,
|
|
89
|
+
description: p.meta.description != null ? String(p.meta.description) : undefined
|
|
90
|
+
}))
|
|
91
|
+
return {
|
|
92
|
+
content: [{
|
|
93
|
+
type: 'text' as const,
|
|
94
|
+
text: JSON.stringify(list, null, 2)
|
|
95
|
+
}]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// ─── Tool: get_nav ──────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
server.tool(
|
|
103
|
+
'get_nav',
|
|
104
|
+
'Get the documentation navigation tree',
|
|
105
|
+
{},
|
|
106
|
+
async () => {
|
|
107
|
+
const nav = await source.getNavTree()
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: 'text' as const,
|
|
111
|
+
text: JSON.stringify(nav, null, 2)
|
|
112
|
+
}]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// ─── Resources: pages ───────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const pageTemplate = new ResourceTemplate(
|
|
120
|
+
'mkdnsite://pages/{slug}',
|
|
121
|
+
{
|
|
122
|
+
list: async () => {
|
|
123
|
+
const pages = await source.listPages()
|
|
124
|
+
return {
|
|
125
|
+
resources: pages.map(p => ({
|
|
126
|
+
uri: `mkdnsite://pages${p.slug}`,
|
|
127
|
+
name: String(p.meta.title ?? p.slug),
|
|
128
|
+
description: p.meta.description != null ? String(p.meta.description) : undefined,
|
|
129
|
+
mimeType: 'text/markdown'
|
|
130
|
+
}))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
server.resource(
|
|
137
|
+
'page',
|
|
138
|
+
pageTemplate,
|
|
139
|
+
{ mimeType: 'text/markdown' },
|
|
140
|
+
async (uri) => {
|
|
141
|
+
// uri.href = "mkdnsite://pages/docs/getting-started"
|
|
142
|
+
const rawSlug = uri.href.replace(/^mkdnsite:\/\/pages/, '')
|
|
143
|
+
const resolvedSlug = rawSlug === '' ? '/' : rawSlug
|
|
144
|
+
const page = await source.getPage(resolvedSlug)
|
|
145
|
+
if (page == null) {
|
|
146
|
+
return { contents: [] }
|
|
147
|
+
}
|
|
148
|
+
const lines: string[] = []
|
|
149
|
+
if (page.meta.title != null) lines.push(`# ${String(page.meta.title)}`)
|
|
150
|
+
if (page.meta.description != null) lines.push(`> ${String(page.meta.description)}`)
|
|
151
|
+
if (lines.length > 0) lines.push('')
|
|
152
|
+
lines.push(page.body)
|
|
153
|
+
return {
|
|
154
|
+
contents: [{
|
|
155
|
+
uri: uri.href,
|
|
156
|
+
mimeType: 'text/markdown',
|
|
157
|
+
text: lines.join('\n')
|
|
158
|
+
}]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return server
|
|
164
|
+
}
|
package/src/mcp/stdio.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
2
|
+
import type { MkdnSiteConfig } from '../config/schema.ts'
|
|
3
|
+
import { createSearchIndex } from '../search/index.ts'
|
|
4
|
+
import { createMcpServer } from './server.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run the MCP server over stdio (JSON-RPC via stdin/stdout).
|
|
8
|
+
*
|
|
9
|
+
* Used by the `mkdnsite mcp` subcommand for MCP clients like Claude Desktop
|
|
10
|
+
* that communicate via stdio rather than HTTP. Does NOT start a web server.
|
|
11
|
+
*/
|
|
12
|
+
export async function runMcpStdio (config: MkdnSiteConfig): Promise<void> {
|
|
13
|
+
let source
|
|
14
|
+
|
|
15
|
+
if (config.github?.owner != null && config.github?.repo != null) {
|
|
16
|
+
const { GitHubSource } = await import('../content/github.ts')
|
|
17
|
+
source = new GitHubSource(config.github)
|
|
18
|
+
} else {
|
|
19
|
+
const { FilesystemSource } = await import('../content/filesystem.ts')
|
|
20
|
+
source = new FilesystemSource(config.contentDir)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const searchIndex = createSearchIndex()
|
|
24
|
+
await searchIndex.rebuild(source)
|
|
25
|
+
|
|
26
|
+
const server = createMcpServer({ source, searchIndex })
|
|
27
|
+
const transport = new StdioServerTransport()
|
|
28
|
+
await server.connect(transport)
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a fetch-compatible handler that routes HTTP requests to the MCP server
|
|
6
|
+
* via the Web Standard Streamable HTTP transport.
|
|
7
|
+
*
|
|
8
|
+
* Uses stateless mode (no session management) — each request is independent.
|
|
9
|
+
* A single transport instance handles all concurrent requests.
|
|
10
|
+
*/
|
|
11
|
+
export function createMcpHandler (server: McpServer): (request: Request) => Promise<Response> {
|
|
12
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
13
|
+
sessionIdGenerator: undefined // stateless mode
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
let connectPromise: Promise<void> | null = null
|
|
17
|
+
|
|
18
|
+
async function ensureConnected (): Promise<void> {
|
|
19
|
+
if (connectPromise == null) {
|
|
20
|
+
connectPromise = server.connect(transport)
|
|
21
|
+
}
|
|
22
|
+
await connectPromise
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return async (request: Request): Promise<Response> => {
|
|
26
|
+
await ensureConnected()
|
|
27
|
+
return await transport.handleRequest(request)
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/negotiate/headers.ts
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
import type { ContentSignals } from '../config/schema.ts'
|
|
1
|
+
import type { ContentSignals, CacheConfig } from '../config/schema.ts'
|
|
2
|
+
|
|
3
|
+
function buildCacheControl (maxAge: number, swr: number): string {
|
|
4
|
+
let cc = 'public, max-age=' + String(maxAge)
|
|
5
|
+
if (swr > 0) cc += ', stale-while-revalidate=' + String(swr)
|
|
6
|
+
return cc
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function buildEtag (versionTag: string, slug?: string): string {
|
|
10
|
+
// Sanitize versionTag: strip quotes and non-printable chars to prevent malformed ETag headers
|
|
11
|
+
// eslint-disable-next-line no-control-regex
|
|
12
|
+
const safeVersion = versionTag.replace(/["\\\x00-\x1f]/g, '')
|
|
13
|
+
const tag = slug != null ? safeVersion + '-' + slug.replace(/[^a-zA-Z0-9-]/g, '_') : safeVersion
|
|
14
|
+
return 'W/"' + tag + '"'
|
|
15
|
+
}
|
|
2
16
|
|
|
3
17
|
export function markdownHeaders (
|
|
4
18
|
tokenCount: number | null,
|
|
5
|
-
signals?: ContentSignals
|
|
19
|
+
signals?: ContentSignals,
|
|
20
|
+
cache?: CacheConfig,
|
|
21
|
+
slug?: string
|
|
6
22
|
): Record<string, string> {
|
|
23
|
+
const maxAge = cache?.maxAgeMarkdown ?? 300
|
|
24
|
+
const swr = cache?.staleWhileRevalidate ?? 0
|
|
7
25
|
const headers: Record<string, string> = {
|
|
8
26
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
9
27
|
Vary: 'Accept',
|
|
10
|
-
'Cache-Control':
|
|
28
|
+
'Cache-Control': buildCacheControl(maxAge, swr)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (cache?.versionTag != null && cache.versionTag !== '') {
|
|
32
|
+
headers.ETag = buildEtag(cache.versionTag, slug)
|
|
11
33
|
}
|
|
12
34
|
|
|
13
35
|
if (tokenCount != null) {
|
|
@@ -16,9 +38,9 @@ export function markdownHeaders (
|
|
|
16
38
|
|
|
17
39
|
if (signals != null) {
|
|
18
40
|
const parts: string[] = []
|
|
19
|
-
if (signals.aiTrain !== undefined) parts.push(
|
|
20
|
-
if (signals.search !== undefined) parts.push(
|
|
21
|
-
if (signals.aiInput !== undefined) parts.push(
|
|
41
|
+
if (signals.aiTrain !== undefined) parts.push('ai-train=' + String(signals.aiTrain))
|
|
42
|
+
if (signals.search !== undefined) parts.push('search=' + String(signals.search))
|
|
43
|
+
if (signals.aiInput !== undefined) parts.push('ai-input=' + String(signals.aiInput))
|
|
22
44
|
if (parts.length > 0) {
|
|
23
45
|
headers['Content-Signal'] = parts.join(', ')
|
|
24
46
|
}
|
|
@@ -27,12 +49,18 @@ export function markdownHeaders (
|
|
|
27
49
|
return headers
|
|
28
50
|
}
|
|
29
51
|
|
|
30
|
-
export function htmlHeaders (): Record<string, string> {
|
|
31
|
-
|
|
52
|
+
export function htmlHeaders (cache?: CacheConfig, slug?: string): Record<string, string> {
|
|
53
|
+
const maxAge = cache?.maxAge ?? 300
|
|
54
|
+
const swr = cache?.staleWhileRevalidate ?? 0
|
|
55
|
+
const headers: Record<string, string> = {
|
|
32
56
|
'Content-Type': 'text/html; charset=utf-8',
|
|
33
57
|
Vary: 'Accept',
|
|
34
|
-
'Cache-Control':
|
|
58
|
+
'Cache-Control': buildCacheControl(maxAge, swr)
|
|
35
59
|
}
|
|
60
|
+
if (cache?.versionTag != null && cache.versionTag !== '') {
|
|
61
|
+
headers.ETag = buildEtag(cache.versionTag, slug)
|
|
62
|
+
}
|
|
63
|
+
return headers
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
/**
|