feat: agents get access to backlinks
This commit is contained in:
@@ -429,8 +429,11 @@ export class MCPServer {
|
||||
|
||||
// ── Entity templates ──
|
||||
server.registerResource('post', new ResourceTemplate('bds://posts/{id}', { list: undefined }), { description: 'A single post by ID' }, async (uri, { id }) => {
|
||||
const result = await this.deps.postEngine.getPost(id as string);
|
||||
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result) }] };
|
||||
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 })) };
|
||||
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(enriched) }] };
|
||||
});
|
||||
|
||||
server.registerResource('media-item', new ResourceTemplate('bds://media/{id}', { list: undefined }), { description: 'A single media item by ID' }, async (uri, { id }) => {
|
||||
@@ -480,7 +483,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.',
|
||||
description: 'Search blog posts by query, category, tags, or date range. Each result includes backlinks (posts linking to it).',
|
||||
inputSchema: {
|
||||
query: z.string().optional().describe('Full-text search query'),
|
||||
category: z.string().optional().describe('Filter by category'),
|
||||
@@ -497,11 +500,20 @@ 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[]) => {
|
||||
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 })) };
|
||||
}));
|
||||
};
|
||||
|
||||
if (args.query && !hasFilters) {
|
||||
// Pure text search — use FTS
|
||||
const results = await this.deps.postEngine.searchPosts(args.query);
|
||||
const paginated = results.slice(offset, offset + limit);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(paginated) }] };
|
||||
const enriched = await enrichWithBacklinks(paginated);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(enriched) }] };
|
||||
}
|
||||
|
||||
// Build structural filter
|
||||
@@ -517,13 +529,15 @@ export class MCPServer {
|
||||
const results = await this.deps.postEngine.searchPostsFiltered(
|
||||
args.query, filter, { offset, limit },
|
||||
);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(results) }] };
|
||||
const enriched = await enrichWithBacklinks(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);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(paginated) }] };
|
||||
const enriched = await enrichWithBacklinks(paginated);
|
||||
return { content: [{ type: 'text' as const, text: JSON.stringify(enriched) }] };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -886,7 +886,7 @@ export class OpenCodeManager {
|
||||
},
|
||||
{
|
||||
name: 'read_post',
|
||||
description: 'Read the full content and metadata of a specific blog post by its ID.',
|
||||
description: 'Read the full content and metadata of a specific blog post by its ID. Includes backlinks (posts linking to this post).',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -897,7 +897,7 @@ export class OpenCodeManager {
|
||||
},
|
||||
{
|
||||
name: 'list_posts',
|
||||
description: 'List blog posts with optional filtering by status, category, tags, year, or month. Returns paginated results. The response includes "total" (global post count in the blog) and "filteredTotal" (count matching current filters). Use year/month filters to efficiently narrow to a time period instead of paginating through all posts. Use offset/limit to page through filtered results.',
|
||||
description: 'List blog posts with optional filtering by status, category, tags, year, or month. Returns paginated results. Each post includes backlinks (posts linking to it). The response includes "total" (global post count in the blog) and "filteredTotal" (count matching current filters). Use year/month filters to efficiently narrow to a time period instead of paginating through all posts. Use offset/limit to page through filtered results.',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -1282,11 +1282,15 @@ export class OpenCodeManager {
|
||||
hasMore: false,
|
||||
offset,
|
||||
limit,
|
||||
posts: filteredPosts.map(p => ({
|
||||
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,
|
||||
posts: await Promise.all(filteredPosts.map(async p => {
|
||||
const backlinks = await this.postEngine.getLinkedBy(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 })),
|
||||
};
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1294,6 +1298,7 @@ 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);
|
||||
return {
|
||||
success: true,
|
||||
post: {
|
||||
@@ -1303,6 +1308,7 @@ export class OpenCodeManager {
|
||||
categories: post.categories, tags: post.tags,
|
||||
createdAt: post.createdAt, updatedAt: post.updatedAt,
|
||||
publishedAt: post.publishedAt,
|
||||
backlinks: backlinks.map(b => ({ id: b.id, title: b.title, slug: b.slug })),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1343,10 +1349,14 @@ export class OpenCodeManager {
|
||||
hasMore: offset + limit < filteredTotal,
|
||||
offset,
|
||||
limit,
|
||||
posts: pageItems.map(p => ({
|
||||
id: p.id, title: p.title, slug: p.slug,
|
||||
status: p.status, categories: p.categories,
|
||||
tags: p.tags, createdAt: p.createdAt, updatedAt: p.updatedAt,
|
||||
posts: await Promise.all(pageItems.map(async p => {
|
||||
const backlinks = await this.postEngine.getLinkedBy(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 })),
|
||||
};
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user