feat: posts also get structured forward links

This commit is contained in:
2026-03-01 08:24:24 +01:00
parent cabb1536c3
commit 61bca755e0
4 changed files with 72 additions and 23 deletions

View File

@@ -307,9 +307,9 @@ You can ONLY access information through the tools listed below. Do not claim oth
Available Data Tools:
- get_blog_stats: Get comprehensive blog statistics (total posts, date range, posts per year, tag/category counts, media count). ALWAYS call this first when you need to understand the scope of the data.
- search_posts: Search blog posts using full-text search. Supports category/tag/year/month filters and pagination (offset/limit).
- read_post: Read the full content and metadata of a specific post by ID.
- list_posts: List posts with optional filtering by status, category, tags, year, and month. Supports pagination (offset/limit). Returns "total" (global count) and "filteredTotal" (matching filter). ALWAYS use the year filter when you need posts from a specific year — this is much faster than paginating through all posts.
- search_posts: Search blog posts using full-text search. Supports category/tag/year/month filters and pagination (offset/limit). Results include backlinks and linksTo.
- read_post: Read the full content and metadata of a specific post by ID. Includes backlinks and linksTo.
- list_posts: List posts with optional filtering by status, category, tags, year, and month. Supports pagination (offset/limit). Returns "total" (global count) and "filteredTotal" (matching filter). Includes backlinks and linksTo. ALWAYS use the year filter when you need posts from a specific year — this is much faster than paginating through all posts.
- get_media: Get information about a specific media file by ID.
- list_media: List media files with optional MIME type, year, month, and tag filtering. Supports pagination (offset/limit). Use year/month filters to narrow efficiently.
- view_image: View an image to analyze its visual content. Use this when you need to describe or analyze what an image looks like.

View File

@@ -431,8 +431,15 @@ export class MCPServer {
server.registerResource('post', new ResourceTemplate('bds://posts/{id}', { list: undefined }), { description: 'A single post by ID' }, async (uri, { id }) => {
const postId = id as string;
const result = await this.deps.postEngine.getPost(postId);
const backlinks = await this.deps.postEngine.getLinkedBy(postId);
const enriched = { ...result, backlinks: backlinks.map(b => ({ id: b.id, title: b.title, slug: b.slug })) };
const [backlinks, linksTo] = await Promise.all([
this.deps.postEngine.getLinkedBy(postId),
this.deps.postEngine.getLinksTo(postId),
]);
const enriched = {
...result,
backlinks: backlinks.map(b => ({ id: b.id, title: b.title, slug: b.slug })),
linksTo: linksTo.map(l => ({ id: l.id, title: l.title, slug: l.slug })),
};
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(enriched) }] };
});
@@ -483,7 +490,7 @@ export class MCPServer {
private registerReadTools(server: McpServer): void {
server.registerTool('search_posts', {
title: 'Search Posts',
description: 'Search blog posts by query, category, tags, or date range. Each result includes backlinks (posts linking to it).',
description: 'Search blog posts by query, category, tags, or date range. Each result includes backlinks (posts linking to it) and linksTo (posts it links to).',
inputSchema: {
query: z.string().optional().describe('Full-text search query'),
category: z.string().optional().describe('Filter by category'),
@@ -500,11 +507,18 @@ export class MCPServer {
const offset = args.offset ?? 0;
const limit = args.limit ?? 50;
// Helper: enrich posts with backlinks
const enrichWithBacklinks = async <T extends { id: string }>(posts: T[]) => {
// Helper: enrich posts with backlinks and linksTo
const enrichWithLinks = async <T extends { id: string }>(posts: T[]) => {
return Promise.all(posts.map(async (p) => {
const backlinks = await this.deps.postEngine.getLinkedBy(p.id);
return { ...p, backlinks: backlinks.map(b => ({ id: b.id, title: b.title, slug: b.slug })) };
const [backlinks, linksTo] = await Promise.all([
this.deps.postEngine.getLinkedBy(p.id),
this.deps.postEngine.getLinksTo(p.id),
]);
return {
...p,
backlinks: backlinks.map(b => ({ id: b.id, title: b.title, slug: b.slug })),
linksTo: linksTo.map(l => ({ id: l.id, title: l.title, slug: l.slug })),
};
}));
};
@@ -512,7 +526,7 @@ export class MCPServer {
// Pure text search — use FTS
const results = await this.deps.postEngine.searchPosts(args.query);
const paginated = results.slice(offset, offset + limit);
const enriched = await enrichWithBacklinks(paginated);
const enriched = await enrichWithLinks(paginated);
return { content: [{ type: 'text' as const, text: JSON.stringify(enriched) }] };
}
@@ -529,14 +543,14 @@ export class MCPServer {
const results = await this.deps.postEngine.searchPostsFiltered(
args.query, filter, { offset, limit },
);
const enriched = await enrichWithBacklinks(results);
const enriched = await enrichWithLinks(results);
return { content: [{ type: 'text' as const, text: JSON.stringify(enriched) }] };
}
// Filter-only query (no text search)
const results = await this.deps.postEngine.getPostsFiltered(filter);
const paginated = results.slice(offset, offset + limit);
const enriched = await enrichWithBacklinks(paginated);
const enriched = await enrichWithLinks(paginated);
return { content: [{ type: 'text' as const, text: JSON.stringify(enriched) }] };
});
}

View File

@@ -1283,13 +1283,17 @@ export class OpenCodeManager {
offset,
limit,
posts: await Promise.all(filteredPosts.map(async p => {
const backlinks = await this.postEngine.getLinkedBy(p.id);
const [backlinks, linksTo] = await Promise.all([
this.postEngine.getLinkedBy(p.id),
this.postEngine.getLinksTo(p.id),
]);
return {
id: p.id, title: p.title, slug: p.slug,
excerpt: p.excerpt, status: p.status,
categories: p.categories, tags: p.tags,
createdAt: p.createdAt, updatedAt: p.updatedAt,
backlinks: backlinks.map(b => ({ id: b.id, title: b.title, slug: b.slug })),
linksTo: linksTo.map(l => ({ id: l.id, title: l.title, slug: l.slug })),
};
})),
};
@@ -1298,7 +1302,10 @@ export class OpenCodeManager {
case 'read_post': {
const post = await this.postEngine.getPost(args.postId as string);
if (!post) return { success: false, error: 'Post not found' };
const backlinks = await this.postEngine.getLinkedBy(post.id);
const [backlinks, linksTo] = await Promise.all([
this.postEngine.getLinkedBy(post.id),
this.postEngine.getLinksTo(post.id),
]);
return {
success: true,
post: {
@@ -1309,6 +1316,7 @@ export class OpenCodeManager {
createdAt: post.createdAt, updatedAt: post.updatedAt,
publishedAt: post.publishedAt,
backlinks: backlinks.map(b => ({ id: b.id, title: b.title, slug: b.slug })),
linksTo: linksTo.map(l => ({ id: l.id, title: l.title, slug: l.slug })),
},
};
}
@@ -1350,12 +1358,16 @@ export class OpenCodeManager {
offset,
limit,
posts: await Promise.all(pageItems.map(async p => {
const backlinks = await this.postEngine.getLinkedBy(p.id);
const [backlinks, linksTo] = await Promise.all([
this.postEngine.getLinkedBy(p.id),
this.postEngine.getLinksTo(p.id),
]);
return {
id: p.id, title: p.title, slug: p.slug,
status: p.status, categories: p.categories,
tags: p.tags, createdAt: p.createdAt, updatedAt: p.updatedAt,
backlinks: backlinks.map(b => ({ id: b.id, title: b.title, slug: b.slug })),
linksTo: linksTo.map(l => ({ id: l.id, title: l.title, slug: l.slug })),
};
})),
};