@recapt/mcp 0.0.4-beta → 0.0.6-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tool Catalog — Registry and discovery for MCP tools.
3
+ *
4
+ * Exports:
5
+ * - searchTools: Find tools by natural language query
6
+ * - registerSearchTools: Register the search_tools MCP tool
7
+ * - registerCallTool: Register the call_tool MCP tool
8
+ * - registerToolHandler: Register a tool handler for call_tool to invoke
9
+ */
10
+ export { searchTools, getToolByName, getAllTools, registerSearchTools, } from "./searchTools.js";
11
+ export { registerCallTool, registerToolHandler, getToolHandler, } from "./callTool.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Search Tools — Semantic and keyword search over the tool catalog.
3
+ *
4
+ * Allows the agent to discover specialized tools by describing what
5
+ * it needs in natural language. Uses pre-computed embeddings for
6
+ * fast, accurate matching with fallback to keyword search.
7
+ */
8
+ export interface ToolEntry {
9
+ name: string;
10
+ description: string;
11
+ category: string;
12
+ parameters: Record<string, {
13
+ type: string;
14
+ required?: boolean;
15
+ description?: string;
16
+ }>;
17
+ embedding: number[];
18
+ }
19
+ export declare function searchTools(query: string, limit?: number, queryEmbedding?: number[]): Promise<ToolEntry[]>;
20
+ export declare function getToolByName(name: string): ToolEntry | undefined;
21
+ export declare function getAllTools(): ToolEntry[];
22
+ export declare function registerSearchTools(server: any): void;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Search Tools — Semantic and keyword search over the tool catalog.
3
+ *
4
+ * Allows the agent to discover specialized tools by describing what
5
+ * it needs in natural language. Uses pre-computed embeddings for
6
+ * fast, accurate matching with fallback to keyword search.
7
+ */
8
+ import { z } from "zod";
9
+ import { readFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { dirname, join } from "node:path";
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ let _catalog = null;
15
+ function loadCatalog() {
16
+ if (_catalog)
17
+ return _catalog;
18
+ try {
19
+ const catalogPath = join(__dirname, "toolCatalog.json");
20
+ const raw = readFileSync(catalogPath, "utf-8");
21
+ _catalog = JSON.parse(raw);
22
+ return _catalog;
23
+ }
24
+ catch {
25
+ console.warn("[searchTools] Failed to load toolCatalog.json, using empty catalog");
26
+ _catalog = [];
27
+ return _catalog;
28
+ }
29
+ }
30
+ function cosineSimilarity(a, b) {
31
+ if (a.length === 0 || b.length === 0 || a.length !== b.length)
32
+ return 0;
33
+ let dot = 0;
34
+ for (let i = 0; i < a.length; i++) {
35
+ dot += a[i] * b[i];
36
+ }
37
+ return dot;
38
+ }
39
+ function searchByKeyword(query, limit) {
40
+ const catalog = loadCatalog();
41
+ const normalizedQuery = query.toLowerCase();
42
+ // Extract words, keeping short ones that might be meaningful (e.g., "ux", "js")
43
+ const queryWords = normalizedQuery
44
+ .split(/[\s_-]+/)
45
+ .filter((w) => w.length >= 2);
46
+ // Also check for the full query as a phrase
47
+ const queryPhrases = [normalizedQuery];
48
+ // Common synonyms and related terms
49
+ const synonyms = {
50
+ error: ["console", "js", "javascript", "bug", "crash", "exception"],
51
+ page: ["pages", "route", "url", "path"],
52
+ user: ["users", "session", "visitor"],
53
+ click: ["clicks", "tap", "press", "rage"],
54
+ form: ["forms", "input", "field", "submit"],
55
+ flow: ["flows", "journey", "funnel", "navigation", "path"],
56
+ issue: ["issues", "problem", "bug", "friction"],
57
+ fix: ["fixes", "remediation", "repair", "resolve"],
58
+ compare: ["comparison", "diff", "versus", "cohort"],
59
+ health: ["score", "metrics", "ux"],
60
+ dead: ["unresponsive", "broken", "stuck"],
61
+ rage: ["angry", "frustrated", "frustration"],
62
+ };
63
+ // Expand query words with synonyms
64
+ const expandedWords = new Set(queryWords);
65
+ for (const word of queryWords) {
66
+ if (synonyms[word]) {
67
+ synonyms[word].forEach((syn) => expandedWords.add(syn));
68
+ }
69
+ // Also check if query word is a synonym value
70
+ for (const [key, values] of Object.entries(synonyms)) {
71
+ if (values.includes(word)) {
72
+ expandedWords.add(key);
73
+ }
74
+ }
75
+ }
76
+ return catalog
77
+ .map((tool) => {
78
+ const toolName = tool.name.toLowerCase().replace(/_/g, " ");
79
+ const toolNameParts = tool.name.toLowerCase().split("_");
80
+ const text = `${toolName} ${tool.description} ${tool.category}`.toLowerCase();
81
+ let score = 0;
82
+ // Phrase match in description (highest value)
83
+ for (const phrase of queryPhrases) {
84
+ if (phrase.length > 3 && text.includes(phrase))
85
+ score += 5;
86
+ }
87
+ // Word matches
88
+ for (const word of expandedWords) {
89
+ // Exact word in tool name parts (e.g., "page" matches "get_page_metrics")
90
+ if (toolNameParts.includes(word))
91
+ score += 4;
92
+ // Word appears in tool name
93
+ if (toolName.includes(word))
94
+ score += 3;
95
+ // Category match
96
+ if (tool.category.toLowerCase() === word)
97
+ score += 2;
98
+ // Word in description
99
+ if (text.includes(word))
100
+ score += 1;
101
+ }
102
+ return { tool, score };
103
+ })
104
+ .filter((s) => s.score > 0)
105
+ .sort((a, b) => b.score - a.score)
106
+ .slice(0, limit)
107
+ .map((s) => s.tool);
108
+ }
109
+ async function searchBySemantic(query, limit, queryEmbedding) {
110
+ const catalog = loadCatalog();
111
+ return catalog
112
+ .filter((t) => t.embedding && t.embedding.length > 0)
113
+ .map((tool) => ({
114
+ tool,
115
+ score: cosineSimilarity(queryEmbedding, tool.embedding),
116
+ }))
117
+ .sort((a, b) => b.score - a.score)
118
+ .slice(0, limit)
119
+ .map((s) => s.tool);
120
+ }
121
+ export async function searchTools(query, limit = 5, queryEmbedding) {
122
+ if (queryEmbedding && queryEmbedding.length > 0) {
123
+ return searchBySemantic(query, limit, queryEmbedding);
124
+ }
125
+ return searchByKeyword(query, limit);
126
+ }
127
+ export function getToolByName(name) {
128
+ return loadCatalog().find((t) => t.name === name);
129
+ }
130
+ export function getAllTools() {
131
+ return loadCatalog();
132
+ }
133
+ const DEFAULT_TOOL_LIMIT = 5;
134
+ const searchToolsSchema = z.object({
135
+ query: z
136
+ .string()
137
+ .describe("Natural language description of what data or analysis capability you need. " +
138
+ "Describe what you want to learn or investigate."),
139
+ limit: z
140
+ .number()
141
+ .optional()
142
+ .describe("Maximum number of tools to return (default 5)"),
143
+ });
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ export function registerSearchTools(server) {
146
+ server.registerTool("search_tools", {
147
+ description: "Discover analysis tools by describing what data or capability you need. " +
148
+ "You have access to 40+ specialized tools covering sessions, pages, behaviors, journeys, " +
149
+ "forms, errors, performance, cohorts, issues, and remediation. Keyword search matches your intent to " +
150
+ "relevant tools. Returns tool names, descriptions, categories, and parameters. " +
151
+ "Use call_tool to execute discovered tools.",
152
+ inputSchema: searchToolsSchema,
153
+ }, async ({ query, limit }) => {
154
+ const searchLimit = limit ?? DEFAULT_TOOL_LIMIT;
155
+ const matches = await searchTools(query, searchLimit);
156
+ if (matches.length === 0) {
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: JSON.stringify({
162
+ tools: [],
163
+ message: "No matching tools found. Try rephrasing your query or use broader terms like 'page', 'session', 'issue', 'flow', or 'form'.",
164
+ }),
165
+ },
166
+ ],
167
+ };
168
+ }
169
+ const tools = matches.map((tool) => ({
170
+ name: tool.name,
171
+ description: tool.description.length > 250
172
+ ? tool.description.slice(0, 250) + "..."
173
+ : tool.description,
174
+ category: tool.category,
175
+ parameters: Object.entries(tool.parameters).map(([name, prop]) => ({
176
+ name,
177
+ type: prop.type,
178
+ required: prop.required ?? false,
179
+ description: prop.description,
180
+ })),
181
+ }));
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: JSON.stringify({
187
+ tools,
188
+ usage: "Use call_tool with the tool name and arguments to execute. Example: call_tool({ tool_name: 'get_page_metrics', arguments: { page_path: '/checkout' } })",
189
+ }),
190
+ },
191
+ ],
192
+ };
193
+ });
194
+ }