Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .changeset/public-carrots-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---

---
Comment on lines +1 to +3
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

Changeset requires meaningful content for this public package.

An empty changeset is only appropriate for private packages. Since @lynx-js/docs-mcp-server is a new public package, the changeset should document the addition with a version bump and description. Based on learnings, public packages require meaningful changelog entries.

Add changeset content describing this new package:

---
-
+@lynx-js/docs-mcp-server: minor
---

+Introduce a new MCP server for providing Lynx documentation resources to LLM clients. The server fetches and registers Markdown documentation from a configurable base URL with automatic resource discovery and content resolution.
📝 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
---
---
---
@lynx-js/docs-mcp-server: minor
---
Introduce a new MCP server for providing Lynx documentation resources to LLM clients. The server fetches and registers Markdown documentation from a configurable base URL with automatic resource discovery and content resolution.
🧰 Tools
🪛 LanguageTool

[grammar] ~1-~1: Hier könnte ein Fehler sein.
Context: --- ---

(QB_NEW_DE)

🤖 Prompt for AI Agents
.changeset/public-carrots-search.md lines 1-3: the changeset file is empty which
is invalid for a new public package; update this file to include a meaningful
changeset entry that names the package @lynx-js/docs-mcp-server, specifies the
new version bump (e.g., minor or patch as appropriate), and includes a short
descriptive summary of the change (e.g., "add new public package
@lynx-js/docs-mcp-server: initial release — implements X and Y"), then save the
file so the changeset contains valid YAML/frontmatter and a description for the
changelog.

109 changes: 109 additions & 0 deletions packages/mcp-servers/docs-mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# @lynx-js/docs-mcp-server

> A MCP Server providing Lynx documentation resources for LLMs, with carefully designed prompting.

`@lynx-js/docs-mcp-server` lets your coding agent (such as Gemini, Claude, Cursor or Copilot)
access Lynx documentation to assist you in development tasks. Therefore,
we have specifically optimized [llms.txt](https://lynxjs.org/next/llms.txt),
a condensed version of the documentation site optimized for reading large models.

## Requirements

- [Node.js](https://nodejs.org/) v18.17 or a newer [latest maintenance LTS](https://github.com/nodejs/Release#release-schedule) version.

## Getting started

Add the following config to your MCP client:

```json
{
"mcpServers": {
"lynx-docs": {
"command": "npx",
"args": [
"-y",
"@lynx-js/docs-mcp-server@latest"
]
}
}
}
```

`@lynx-js/docs-mcp-server` works best with MCP clients that supports [Server Instructions](https://modelcontextprotocol.io/specification/draft/schema#initializeresult), such as Claude Code.
If you find your MCP client don't know about the MCP server,
you can manually provide the following instructions
(e.g. in your `AGENTS.md`, `CLAUDE.md`, or just send it along with your question):

```md
For any questions or requirements regarding Lynx:

1. Use the "List Resources Tool" to list all Resources provided in MCP "lynx-docs".
2. First read MCP Resources "lynx-docs://llms.txt" (**REQUIRED**), this document is an ENTRYPOINT of all Lynx Docs.
3. After reading "lynx-docs://llms.txt", use the "Read MCP Resources Tool" to retrieve docs you need based on the user's questions or requirements, please read them proactively.
4. If available, prioritize obtaining Lynx-related information through MCP Resources tools over external web searches.
```

<details>
<summary>Claude Code</summary>
Use the Claude Code CLI to add the Lynx Docs MCP server (<a href="https://docs.anthropic.com/en/docs/claude-code/mcp">guide</a>):

```bash
claude mcp add lynx-docs npx @lynx-js/docs-mcp-server@latest
```

</details>

<details>
<summary>Codex</summary>
Follow the <a href="https://github.com/openai/codex/blob/main/docs/advanced.md#model-context-protocol-mcp">configure MCP guide</a>
using the standard config from above. You can also install the Lynx Docs MCP server using the Codex CLI:

```bash
codex mcp add lynx-docs -- npx @lynx-js/docs-mcp-server@latest
```

</details>

<details>
<summary>Copilot / VS Code</summary>
Follow the MCP install <a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server">guide</a>,
with the standard config from above. You can also install the Lynx Docs MCP server using the VS Code CLI:

```bash
code --add-mcp '{"name":"lynx-docs","command":"npx","args":["@lynx-js/docs-mcp-server@latest"]}'
```

</details>

<details>
<summary>Cursor</summary>

**Install manually:**

Go to `Cursor Settings` -> `MCP` -> `New MCP Server`. Use the config provided above.

</details>

<details>
<summary>Gemini CLI</summary>
Install the Lynx Docs MCP server using the Gemini CLI.

**Project wide:**

```bash
gemini mcp add lynx-docs npx @lynx-js/docs-mcp-server@latest
```

**Globally:**

```bash
gemini mcp add -s user lynx-docs npx @lynx-js/docs-mcp-server@latest
```

Alternatively, follow the <a href="https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#how-to-set-up-your-mcp-server">MCP guide</a> and use the standard config from above.

</details>

## Credits

This project is inspired by [Svelte MCP server](https://svelte.dev/docs/mcp/overview). Both the implementation and documentation have been adapted and referenced from the original MCP server.
207 changes: 207 additions & 0 deletions packages/mcp-servers/docs-mcp-server/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
#!/usr/bin/env node

// Copyright 2025 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { readFile } from 'node:fs/promises';

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Command } from 'commander';
import createDebug from 'debug';
import * as findPackage from 'empathic/package';
import type { Link, Node } from 'mdast';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { toMarkdown } from 'mdast-util-to-markdown';
import { fetch } from 'undici';

// NOTE: Un comment below to enable caching and debug for undici fetch requests
// import {
// interceptors,
// EnvHttpProxyAgent
// setGlobalDispatcher,
// // @ts-expect-error missing types
// cacheStores,
// } from 'undici';
// const agent = new EnvHttpProxyAgent().compose(
// interceptors.cache({
// store: new cacheStores.MemoryCacheStore({
// maxSize: 100 * 1024 * 1024, // 100MB
// maxCount: 1000,
// maxEntrySize: 5 * 1024 * 1024, // 5MB
// }),
// methods: ['GET', 'HEAD'], // Optional: specify which methods to cache
// }),
// );
// setGlobalDispatcher(agent);

const debug = createDebug('lynx-docs-mcp');

const pkgPath = findPackage.up({ cwd: new URL('.', import.meta.url).pathname });
const pkg = JSON.parse(await readFile(pkgPath!, 'utf-8')) as {
version: string;
name: string;
description: string;
};

const MCP_SERVER_NAME = 'lynx-docs';

function registerResources(
baseURL: string,
mcpServer: McpServer,
fromMarkdownText: string,
) {
const tree = fromMarkdown(fromMarkdownText); // verify markdown is valid

const forEachLink = (node: Node, cb: (link: Link) => void) => {
if (node.type === 'link') {
cb(node as Link);
} else if ('children' in node && Array.isArray(node.children)) {
for (const child of node.children) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
forEachLink(child, cb);
}
}
};

const linkUrls: Map<string, Link> = new Map();
forEachLink(tree, (link) => {
try {
const base = new URL(baseURL);
const u = new URL(link.url);
if (
u.hostname === base.hostname
// Some links may be absolute URLs to lynxjs.org
|| u.hostname === 'lynxjs.org'
) {
// Strip versioned path prefixes like /next/ or /1.2.3/
const strippedUrl = u.pathname.replace(
/^\/(?:next|\d+(?:\.\d+)*)?\/?/,
'',
);
linkUrls.set(strippedUrl, link);
}
} catch {
// Ignore invalid URLs
}
});

linkUrls.forEach((link, strippedUrl) => {
// Generate a title for the resource by converting the link node back to markdown
// NOTE: The title generation is complex because link titles may contain nested formatting, DON'T just use link.title
const title = toMarkdown({ ...link, type: 'root' }).trim();

if (!title) {
return;
}

debug(
`Registering resource: lynx-docs://${strippedUrl} from [${title}](${baseURL}/${strippedUrl})`,
);

mcpServer.registerResource(
title,
`lynx-docs://${strippedUrl}`,
{
title,
description: title,
mimeType: 'text/markdown',
},
async () => ({
contents: [
{
uri: `lynx-docs://${strippedUrl}`,
text: await fetch(link.url).then((res) => res.text()),
mimeType: 'text/markdown',
},
],
}),
);
Comment on lines +110 to +119
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

Add error handling for content resolution fetch.

The dynamic content resolver on line 113 lacks error handling. If the fetch fails or the URL is inaccessible, the error will propagate unhandled and crash the MCP server. Wrap the fetch in a try-catch to provide graceful error handling.

    async () => ({
      contents: [
        {
          uri: `lynx-docs://${strippedUrl}`,
-         text: await fetch(link.url).then((res) => res.text()),
+         text: await fetch(link.url)
+           .then((res) => {
+             if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
+             return res.text();
+           })
+           .catch((error) => {
+             debug(`Failed to fetch ${link.url}:`, error);
+             throw new Error(`Cannot retrieve content for ${link.url}`);
+           }),
          mimeType: 'text/markdown',
        },
      ],
    }),
📝 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
async () => ({
contents: [
{
uri: `lynx-docs://${strippedUrl}`,
text: await fetch(link.url).then((res) => res.text()),
mimeType: 'text/markdown',
},
],
}),
);
async () => ({
contents: [
{
uri: `lynx-docs://${strippedUrl}`,
text: await fetch(link.url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
return res.text();
})
.catch((error) => {
debug(`Failed to fetch ${link.url}:`, error);
throw new Error(`Cannot retrieve content for ${link.url}`);
}),
mimeType: 'text/markdown',
},
],
}),
🤖 Prompt for AI Agents
In packages/mcp-servers/docs-mcp-server/main.ts around lines 109 to 118, the
dynamic content resolver calls fetch(link.url) without error handling which can
propagate exceptions and crash the MCP server; wrap the await
fetch(...).then(res => res.text()) in a try-catch, log the error (using the
existing server logger or console.error), and return a safe fallback object with
the same shape (e.g., contents array with a placeholder text or empty string and
appropriate mimeType) so the resolver always resolves gracefully instead of
allowing the exception to bubble.

});
}

async function main(baseUrl: string) {
const ROOT_DOC_URL = `${baseUrl}/llms.txt`;
const ROOT_DOC_MARKDOWN = await fetch(ROOT_DOC_URL).then((res) => res.text());
Comment on lines +123 to +125
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

Add error handling for root document fetch.

The root document fetch on line 124 lacks error handling. If the fetch fails or baseUrl/llms.txt is unreachable, the error will crash the server during startup.

async function main(baseUrl: string) {
  const ROOT_DOC_URL = `${baseUrl}/llms.txt`;
- const ROOT_DOC_MARKDOWN = await fetch(ROOT_DOC_URL).then((res) => res.text());
+ try {
+   const response = await fetch(ROOT_DOC_URL);
+   if (!response.ok) {
+     throw new Error(`Failed to fetch root doc: HTTP ${response.status}`);
+   }
+   const ROOT_DOC_MARKDOWN = await response.text();
+ } catch (error) {
+   debug(`Error fetching root documentation from ${ROOT_DOC_URL}:`, error);
+   throw error;
+ }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/mcp-servers/docs-mcp-server/main.ts around lines 122 to 124, the
fetch of the root document (ROOT_DOC_URL) has no error handling; wrap the fetch
and text extraction in a try/catch (or check response.ok) to handle network or
non-2xx responses, log the error with context, and fallback to a safe default
(e.g., empty string or cached content) so server startup does not crash; ensure
the function returns or continues safely when the root doc cannot be retrieved.

const ROOT_DOC_RESOURCE_URI = 'lynx-docs://llms.txt';
const ROOT_DOC_TITLE = 'Read This Before Building with Lynx';
const ROOT_DOC_DESCRIPTION =
'Before building with Lynx, be sure to read this document to understand important information and best practices.';

const mcpServer = new McpServer({
name: MCP_SERVER_NAME,
version: pkg.version,
}, {
// NOTE: This instruction for now is not supported by Codex's MCP support,
// see https://github.com/openai/codex/issues/6148
Comment on lines +135 to +136
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

The comment references 'Codex', which appears to be incorrect. Based on the context of MCP (Model Context Protocol), this should likely reference 'Claude' or another LLM client, not OpenAI's Codex.

Suggested change
// NOTE: This instruction for now is not supported by Codex's MCP support,
// see https://github.com/openai/codex/issues/6148
// NOTE: This instruction is not currently supported by most MCP clients (e.g., Claude).
// See the relevant client documentation or issue trackers for updates.

Copilot uses AI. Check for mistakes.
instructions: `\
<user_instructions>
For any questions or requirements regarding Lynx:

1. Use the "List Resources Tool" to list all Resources provided in MCP "${MCP_SERVER_NAME}".
2. First read MCP Resources "${ROOT_DOC_RESOURCE_URI}" (**REQUIRED**), this document is an ENTRYPOINT of all Lynx Docs.
3. After reading "${ROOT_DOC_RESOURCE_URI}", use the "Read MCP Resources Tool" to retrieve docs you need based on the user's questions or requirements, please read them proactively.
4. If available, prioritize obtaining Lynx-related information through MCP Resources tools over external web searches.

</user_instructions>
`,
});

mcpServer.registerResource(
ROOT_DOC_TITLE,
ROOT_DOC_RESOURCE_URI,
{
title: ROOT_DOC_TITLE,
description: ROOT_DOC_DESCRIPTION,
mimeType: 'text/markdown',
},
() => ({
contents: [
{
uri: ROOT_DOC_RESOURCE_URI,
text: ROOT_DOC_MARKDOWN,
mimeType: 'text/markdown',
},
],
}),
);

registerResources(baseUrl, mcpServer, ROOT_DOC_MARKDOWN);

const transport = new StdioServerTransport();
await mcpServer.connect(transport);
}

const program = new Command();

program
.name(`npx -y ${pkg.name}`)
.description(pkg.description)
.option(
'--base-url <url>',
'Base URL for fetching Lynx docs. Set if you want versioned docs.',
'https://lynxjs.org/next/',
)
.version(pkg.version)
.addHelpText(
'after',
`
Usage as a MCP Server:
{
"mcpServers": {
"${MCP_SERVER_NAME}": {
"command": "npx",
"args": ["-y", "${pkg.name}"]
}
}
}
`,
)
.action(async (options: { baseUrl: string }) => {
await main(
// need to remove trailing slash if any
options.baseUrl.replace(/\/+$/, ''),
);
});

program.parse(process.argv);
34 changes: 34 additions & 0 deletions packages/mcp-servers/docs-mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@lynx-js/docs-mcp-server",
"version": "0.2.1",
"description": "A MCP Server providing Lynx documentation resources for LLMs, with carefully designed prompting.",
"type": "module",
"bin": "./main.ts",
"files": [
"dist",
"main.ts"
],
"scripts": {
"build": "tsc"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.20.0",
"commander": "^13.1.0",
"debug": "^4.4.3",
"empathic": "^2.0.0",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-to-markdown": "^2.1.2",
"undici": "^6.22.0"
},
"devDependencies": {
"@types/debug": "^4.1.12",
"@types/mdast": "^4.0.4",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=18.17"
},
"publishConfig": {
"bin": "./dist/main.js"
}
}
11 changes: 11 additions & 0 deletions packages/mcp-servers/docs-mcp-server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": false,
"isolatedDeclarations": false,
"emitDeclarationOnly": false,
"types": ["node"],
"outDir": "dist",
},
"include": ["src", "main.ts"],
Comment on lines +8 to +10
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

Trailing comma on line 8 in a JSON file will cause parse errors. JSON does not allow trailing commas before closing braces.

Suggested change
"outDir": "dist",
},
"include": ["src", "main.ts"],
"outDir": "dist"
},
"include": ["src", "main.ts"]

Copilot uses AI. Check for mistakes.
}
Loading
Loading