mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-11-02 18:23:49 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6b8502036 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix the MCP `getPage` tool returning "Page not found" for the Markdown URL of a page (`<page>.md`).
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Ignore tag category references when displaying assigned tags and update filters.
|
||||
@@ -145,11 +145,7 @@ describe('getDocumentSections', () => {
|
||||
type: 'update',
|
||||
data: {
|
||||
date: '2026-04-08',
|
||||
tags: [
|
||||
{ kind: 'tag', tag: 'improvements' },
|
||||
{ kind: 'tag', tag: 'category' },
|
||||
{ kind: 'tag', tag: 'missing' },
|
||||
],
|
||||
tags: [{ kind: 'tag', tag: 'improvements' }],
|
||||
},
|
||||
isVoid: false,
|
||||
nodes: [
|
||||
@@ -201,7 +197,6 @@ describe('getDocumentSections', () => {
|
||||
...context,
|
||||
revision: {
|
||||
tags: [
|
||||
{ slug: 'category', label: 'Products', color: 'default', kind: 'category' },
|
||||
{
|
||||
slug: 'improvements',
|
||||
label: 'Improvements',
|
||||
|
||||
@@ -50,6 +50,16 @@ describe('extractPagePath', () => {
|
||||
it('returns empty string when URL equals root base URL', () => {
|
||||
expect(extractPagePath('https://docs.example.com/', 'https://docs.example.com/')).toBe('');
|
||||
});
|
||||
|
||||
it('extracts path from the markdown URL of a page', () => {
|
||||
expect(
|
||||
extractPagePath('https://docs.example.com/api/guides/installation.md', baseURL)
|
||||
).toBe('guides/installation');
|
||||
expect(extractPagePath('https://docs.example.com/api/.md', baseURL)).toBe('');
|
||||
expect(extractPagePath('https://docs.example.com/api/notes.md.html', baseURL)).toBe(
|
||||
'notes.md.html'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveFirstDocument', () => {
|
||||
|
||||
@@ -412,7 +412,8 @@ function flattenPages(
|
||||
* Returns the path segment after the base, or undefined if the URL doesn't match.
|
||||
*/
|
||||
export function extractPagePath(url: string, baseURL: string): string | undefined {
|
||||
const urlPath = getURLPathname(url);
|
||||
// Pages are also served as Markdown at `<page>.md`, the form llms.txt links to.
|
||||
const urlPath = getURLPathname(url)?.replace(/\.md$/, '');
|
||||
const basePath = getURLPathname(baseURL);
|
||||
|
||||
if (urlPath === undefined || basePath === undefined) {
|
||||
|
||||
@@ -143,30 +143,6 @@ describe('resolveStringContentRef', () => {
|
||||
});
|
||||
|
||||
describe('resolveContentRef', () => {
|
||||
it('does not resolve category or missing assignments as tag links', async () => {
|
||||
const normal = { slug: 'constructor', label: 'Constructor', color: 'default' };
|
||||
const category = {
|
||||
slug: 'products',
|
||||
label: 'Products',
|
||||
color: 'default',
|
||||
kind: 'category',
|
||||
};
|
||||
const context = {
|
||||
space: { id: 'local' },
|
||||
revision: { tags: [normal, category] },
|
||||
linker: createLinker({
|
||||
host: 'docs.example.com',
|
||||
spaceBasePath: '/',
|
||||
siteBasePath: '/',
|
||||
}),
|
||||
} as unknown as GitBookAnyContext;
|
||||
expect(await resolveContentRef({ kind: 'tag', tag: 'products' }, context)).toBeNull();
|
||||
expect(await resolveContentRef({ kind: 'tag', tag: 'missing' }, context)).toBeNull();
|
||||
expect(
|
||||
await resolveContentRef({ kind: 'tag', tag: 'constructor', space: 'local' }, context)
|
||||
).toMatchObject({ text: 'Constructor' });
|
||||
});
|
||||
|
||||
it("resolves a page in a different space using that space's page data, not the current context page", async () => {
|
||||
const sharedPageId = 'page-shared';
|
||||
|
||||
|
||||
@@ -8,12 +8,10 @@ export function getRevisionTags(revision: Revision | undefined): RevisionTag[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an assignment to a normal tag, ignoring categories and missing definitions.
|
||||
* Resolve a tag slug to its full RevisionTag.
|
||||
*/
|
||||
export function resolveTag(slug: string, revisionTags: RevisionTag[]): RevisionTag | undefined {
|
||||
return revisionTags.find(
|
||||
(tag) => tag.slug === slug && !('kind' in tag && tag.kind === 'category')
|
||||
);
|
||||
return revisionTags.find((t) => t.slug === slug);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import {
|
||||
DocumentTextColor,
|
||||
type JSONDocument,
|
||||
type Revision,
|
||||
type RevisionTag,
|
||||
} from '@gitbook/api';
|
||||
import type { JSONDocument, Revision } from '@gitbook/api';
|
||||
|
||||
import { getRevisionTags, resolveBlockTags, resolveTag } from './tags';
|
||||
import {
|
||||
UPDATES_TAG_FILTER_CAP,
|
||||
getDocumentFilterableTags,
|
||||
@@ -15,40 +9,6 @@ import {
|
||||
} from './updates';
|
||||
|
||||
describe('getDocumentFilterableTags', () => {
|
||||
it('ignores category and deleted references without removing their stored assignments', () => {
|
||||
const document = createUpdatesDocument([['category', 'constructor', 'missing']]);
|
||||
const category = { ...createTag('category', 'Products'), kind: 'category' };
|
||||
const normal = createTag('constructor', 'Constructor');
|
||||
const revision = { tags: [category, normal] } as unknown as Revision;
|
||||
const before = JSON.stringify(document);
|
||||
expect(getDocumentFilterableTags(document, revision).map((tag) => tag.slug)).toEqual([
|
||||
'constructor',
|
||||
]);
|
||||
expect(resolveTag('category', revision.tags)).toBeUndefined();
|
||||
expect(resolveTag('missing', revision.tags)).toBeUndefined();
|
||||
expect(resolveTag('constructor', revision.tags)).toEqual(normal);
|
||||
expect(getRevisionTags(revision)).toEqual([category, normal]);
|
||||
expect(JSON.stringify(document)).toBe(before);
|
||||
});
|
||||
|
||||
it('rejects category assignments even when definitions are passed directly to the resolver', () => {
|
||||
const normal = createTag('beta', 'Beta');
|
||||
const tags = [
|
||||
{ ...createTag('products', 'Products'), kind: 'category' },
|
||||
normal,
|
||||
] satisfies (RevisionTag & { kind?: 'category' })[];
|
||||
expect(
|
||||
resolveBlockTags(
|
||||
[
|
||||
{ kind: 'tag', tag: 'products' },
|
||||
{ kind: 'tag', tag: 'beta' },
|
||||
{ kind: 'tag', tag: 'missing' },
|
||||
],
|
||||
tags
|
||||
)
|
||||
).toEqual([normal]);
|
||||
});
|
||||
|
||||
it('returns unique update tags in document order', () => {
|
||||
const document = createUpdatesDocument([
|
||||
['improvements', 'fixes'],
|
||||
@@ -117,11 +77,11 @@ function createUpdatesDocument(entryTags: string[][]): JSONDocument {
|
||||
};
|
||||
}
|
||||
|
||||
function createTag(slug: string, label: string): RevisionTag {
|
||||
function createTag(slug: string, label: string) {
|
||||
return {
|
||||
slug,
|
||||
label,
|
||||
color: DocumentTextColor.Default,
|
||||
color: 'default',
|
||||
icon: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,6 +55,29 @@ it(
|
||||
{ timeout: 15_000 }
|
||||
);
|
||||
|
||||
it(
|
||||
'should get a page through MCP from its markdown URL',
|
||||
async () => {
|
||||
const client = await connectMCPClient(
|
||||
getContentTestURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/~gitbook/mcp/auth'
|
||||
)
|
||||
);
|
||||
|
||||
const response = await client.callTool({
|
||||
name: 'getPage',
|
||||
arguments: {
|
||||
url: 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/3.0/other-page.md',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.isError).toBeFalsy();
|
||||
// @ts-expect-error - response.content is of type unknown
|
||||
expect(response.content[0]?.text).toContain('# Other Page');
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
);
|
||||
|
||||
describe('MCP on a site behind visitor authentication', () => {
|
||||
const VA_SITE_URL = 'https://gitbook-open-e2e-sites.gitbook.io/va-site-redirects-fallback';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user