feat(mcpedia): Phase 2 — semantic + hybrid search, tRPC/Hono API

- @mcpedia/embeddings: OpenRouter provider (9router /v1, encoding_format float),
  chunkText + embedChunks; EMBED_DIM=2048
- document_chunks table (real[] embedding) — pgvector NOT available on shared
  imrnes Postgres, so cosine is computed in-app (KB-scale fine); pgvector deferred
- indexer: chunk + embed + upsert per document
- @mcpedia/search: semanticSearch (cosine) + hybridSearch (FTS+cosine RRF)
- apps/api: Hono + tRPC v11 (6 procedures), serve on :4020
- MCP: semantic_search + hybrid_search tools (6 total)
- web: keyword/hybrid toggle; .env.example + README + PHASES updated
This commit is contained in:
asepharyana committed 2026-08-19 19:00:29 +07:00
1 parent ec3a2867d4
commit 9397303f01
32 files changed
+1120 -42

No files matched your search

+37 -2
View File
@@ -1,8 +1,7 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { listDocuments, getDocument, getRelated } from "@mcpedia/core";
import { keywordSearch } from "@mcpedia/search";
import { listDocuments, getDocument, getRelated, semanticSearch, hybridSearch, keywordSearch } from "@mcpedia/core";
export function createMcpServer(): McpServer {
const server = new McpServer({
@@ -84,6 +83,42 @@ export function createMcpServer(): McpServer {
},
);
server.registerTool(
"semantic_search",
{
description:
"Semantic (embedding) search across chunked document content. Best for conceptual/paraphrased queries that don't share exact keywords. Returns chunks ranked by cosine similarity.",
inputSchema: z.object({
query: z.string().describe("Natural-language query"),
limit: z.number().int().positive().max(50).optional(),
}),
},
async ({ query, limit }) => {
const hits = await semanticSearch(query, limit ?? 10);
return {
content: [{ type: "text", text: JSON.stringify(hits, null, 2) }],
};
},
);
server.registerTool(
"hybrid_search",
{
description:
"Hybrid search fusing full-text (Postgres FTS) and semantic (embedding) signals via Reciprocal Rank Fusion. Best general-purpose search.",
inputSchema: z.object({
query: z.string().describe("Free-text or natural-language query"),
limit: z.number().int().positive().max(50).optional(),
}),
},
async ({ query, limit }) => {
const hits = await hybridSearch(query, limit ?? 10);
return {
content: [{ type: "text", text: JSON.stringify(hits, null, 2) }],
};
},
);
return server;
}