Skip to content
Draft
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
32 changes: 32 additions & 0 deletions docs/.vitepress/authInfoLinks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { AUTH_INFO_INTERFACE_ANCHOR, correctAuthInfoTypeHref, isAuthInfoInterfacePage } from './authInfoLinks.ts';

const server = 'api/index/@modelcontextprotocol/server/index.md';
const client = 'api/index/@modelcontextprotocol/client/index.md';
const middleware = 'api/@modelcontextprotocol/server/server/middleware/bearerAuth.md';

test('public client/server interface pages receive a stable target', () => {
assert.equal(AUTH_INFO_INTERFACE_ANCHOR, 'authinfo-interface');
assert.equal(isAuthInfoInterfacePage(server), true);
assert.equal(isAuthInfoInterfacePage(client), true);
assert.equal(isAuthInfoInterfacePage(middleware), false);
});

test('correct AuthInfo type links but never member links', () => {
const href = '../../../../index/@modelcontextprotocol/server/index.md#authinfo';
assert.equal(correctAuthInfoTypeHref(href, 'AuthInfo', middleware), href.replace('#authinfo', '#authinfo-interface'));
assert.equal(correctAuthInfoTypeHref(href, 'authInfo', middleware), href);
const sibling = '../../../../index/@modelcontextprotocol/server/index.md#authinfo-1';
assert.equal(correctAuthInfoTypeHref(sibling, 'AuthInfo', middleware), sibling);
});

test('preserve unrelated local anchors and targets', () => {
assert.equal(correctAuthInfoTypeHref('#authinfo', 'AuthInfo', server), '#authinfo-interface');
assert.equal(correctAuthInfoTypeHref('#authinfo', 'authInfo', server), '#authinfo');
assert.equal(correctAuthInfoTypeHref('#authinfo', 'AuthInfo', middleware), '#authinfo');
const unrelated = '../../../../auth/index.md#authinfo';
assert.equal(correctAuthInfoTypeHref(unrelated, 'AuthInfo', middleware), unrelated);
const clientHref = '../../../index/@modelcontextprotocol/client/index.md#authinfo';
assert.equal(correctAuthInfoTypeHref(clientHref, 'AuthInfo', middleware), clientHref.replace('#authinfo', '#authinfo-interface'));
});
17 changes: 17 additions & 0 deletions docs/.vitepress/authInfoLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** Stable anchor for the public AuthInfo interface, distinct from authInfo members. */
export const AUTH_INFO_INTERFACE_ANCHOR = 'authinfo-interface';

const typePage = /^api\/index\/@modelcontextprotocol\/(?:client|server)\/index\.md$/;
const typeTarget = /(?:^|\/)index\/@modelcontextprotocol\/(?:client|server)\/index\.md$/;

export function isAuthInfoInterfacePage(page: string): boolean {
return typePage.test(page);
}

/** Rewrite only links whose *text* names the AuthInfo type, never member links. */
export function correctAuthInfoTypeHref(href: string, label: string | undefined, page: string): string {
if (label !== 'AuthInfo' || !href.endsWith('#authinfo')) return href;
const path = href.slice(0, -'#authinfo'.length);
if (path ? !typeTarget.test(path) : !isAuthInfoInterfacePage(page)) return href;
return `${path}#${AUTH_INFO_INTERFACE_ANCHOR}`;
}
19 changes: 19 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { defineConfig, type DefaultTheme } from 'vitepress';
import { generateLlmsArtifacts } from './llms';
import { guideSidebar } from './nav';
import { githubSlug, rewriteSkillLink } from './skill';
import { AUTH_INFO_INTERFACE_ANCHOR, correctAuthInfoTypeHref, isAuthInfoInterfacePage } from './authInfoLinks';

const docsDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');

Expand Down Expand Up @@ -44,11 +45,29 @@ export default defineConfig({
const orig = md.renderer.rules.link_open ?? ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const href = tokens[idx].attrGet('href');
if (href) {
const corrected = correctAuthInfoTypeHref(href, tokens[idx + 1]?.content, env.relativePath);
if (corrected !== href) tokens[idx].attrSet('href', corrected);
}
if (href?.startsWith('/specification/')) {
tokens[idx].attrSet('href', `https://modelcontextprotocol.io${href}`);
}
return orig(tokens, idx, options, env, self);
};
// AuthInfo is a public interface, but earlier authInfo member headings win the
// auto-generated #authinfo slug. Keep all existing headings/links intact and
// give only the interface a stable second anchor.
md.core.ruler.push('authinfo_interface_anchor', state => {
if (!isAuthInfoInterfacePage(state.env.relativePath)) return;
const i = state.tokens.findIndex(
(token, index) => token.type === 'heading_open' && state.tokens[index + 1]?.content === 'AuthInfo'
);
if (i < 0) return;
const anchor = new state.Token('html_block', '', 0);
anchor.content = `<a id="${AUTH_INFO_INTERFACE_ANCHOR}"></a>\n`;
state.tokens.splice(i, 0, anchor);
});

// Resolve skill links on the assembled page, then GitHub-style anchors to VitePress's heading ids.
md.core.ruler.push('page_links', state => {
const ids = new Set<string>();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"docs:dev": "vitepress dev docs",
"docs:build": "pnpm docs:api && vitepress build docs",
"docs:multi": "bash scripts/build-docs-site.sh",
"docs:check": "pnpm docs:build",
"docs:check": "tsx --test docs/.vitepress/authInfoLinks.test.ts && pnpm docs:build",
"typecheck:all": "pnpm -r typecheck",
"build:all": "pnpm -r build",
"ci:publish": "pnpm run build:all && pnpm changeset publish",
Expand Down
Loading