prism-mcp-server 1.5.0

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.
Files changed (47) hide show
  1. package/.gitmodules +3 -0
  2. package/Dockerfile +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +970 -0
  5. package/benchmark.ts +172 -0
  6. package/call_chrome_mcp.py +96 -0
  7. package/docker-compose.yml +67 -0
  8. package/execute_via_chrome_mcp.py +133 -0
  9. package/gmail_auth_test.py +29 -0
  10. package/gmail_list_latest_5.py +27 -0
  11. package/index.ts +34 -0
  12. package/list_chrome_tools.py +70 -0
  13. package/package.json +64 -0
  14. package/patch_cgc_mcp.py +90 -0
  15. package/repomix-output.xml +9 -0
  16. package/run_server.sh +9 -0
  17. package/server.json +78 -0
  18. package/src/config.ts +85 -0
  19. package/src/server.ts +627 -0
  20. package/src/tools/compactionHandler.ts +313 -0
  21. package/src/tools/definitions.ts +367 -0
  22. package/src/tools/handlers.ts +261 -0
  23. package/src/tools/index.ts +38 -0
  24. package/src/tools/sessionMemoryDefinitions.ts +437 -0
  25. package/src/tools/sessionMemoryHandlers.ts +774 -0
  26. package/src/utils/braveApi.ts +375 -0
  27. package/src/utils/embeddingApi.ts +97 -0
  28. package/src/utils/executor.ts +105 -0
  29. package/src/utils/googleAi.ts +107 -0
  30. package/src/utils/keywordExtractor.ts +207 -0
  31. package/src/utils/supabaseApi.ts +194 -0
  32. package/supabase/migrations/015_session_memory.sql +145 -0
  33. package/supabase/migrations/016_knowledge_accumulation.sql +315 -0
  34. package/supabase/migrations/017_ledger_compaction.sql +74 -0
  35. package/supabase/migrations/018_semantic_search.sql +110 -0
  36. package/supabase/migrations/019_concurrency_control.sql +320 -0
  37. package/supabase/migrations/020_multi_tenant_rls.sql +459 -0
  38. package/test_cross_mcp.js +393 -0
  39. package/test_mcp_schema.js +83 -0
  40. package/tests/test_knowledge_system.js +319 -0
  41. package/tsconfig.json +16 -0
  42. package/vertex-ai/test_claude_vertex.py +78 -0
  43. package/vertex-ai/test_gemini_vertex.py +39 -0
  44. package/vertex-ai/test_hybrid_search_pipeline.ts +296 -0
  45. package/vertex-ai/test_pipeline_benchmark.ts +251 -0
  46. package/vertex-ai/test_realworld_comparison.ts +290 -0
  47. package/vertex-ai/verify_discovery_engine.ts +72 -0
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Ledger Compaction Handler (v1.5.0 — Enhancement #2)
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * REVIEWER NOTE: This module implements automatic rollup of old
6
+ * ledger entries to prevent unbounded growth.
7
+ *
8
+ * THE PROBLEM:
9
+ * session_ledger is append-only. After months of heavy use,
10
+ * the "deep" context loading could return thousands of tokens
11
+ * or hit API payload limits. There's no automatic cleanup.
12
+ *
13
+ * THE SOLUTION:
14
+ * When triggered, this handler:
15
+ * 1. Calls get_compaction_candidates() to find bloated projects
16
+ * 2. For each project: fetches old entries, summarizes via Gemini
17
+ * 3. Inserts a rollup entry with is_rollup=true
18
+ * 4. Marks old entries with archived_at (soft-delete)
19
+ *
20
+ * CHUNKING STRATEGY:
21
+ * If a project has 50 entries to compact, we don't send all 50
22
+ * to Gemini at once (might blow past token limits). Instead:
23
+ * - Chunk into groups of 10
24
+ * - Summarize each chunk → get 5 sub-summaries
25
+ * - If 5+ sub-summaries exist, summarize those into a final rollup
26
+ * This recursive strategy ensures we never exceed Gemini's context window.
27
+ *
28
+ * SAFETY:
29
+ * - Old entries are soft-deleted (archived_at set), not hard-deleted
30
+ * - The rollup entry preserves all keywords and decisions from originals
31
+ * - Reversible: set archived_at=NULL to restore original entries
32
+ * - Dry run mode available to preview before executing
33
+ * ═══════════════════════════════════════════════════════════════════
34
+ */
35
+
36
+ import { supabaseRpc, supabaseGet, supabasePost, supabasePatch } from "../utils/supabaseApi.js";
37
+ import { GOOGLE_API_KEY, PRISM_USER_ID } from "../config.js";
38
+ import { GoogleGenerativeAI } from "@google/generative-ai";
39
+
40
+ // ─── Constants ────────────────────────────────────────────────
41
+
42
+ // REVIEWER NOTE: Chunk size for sending entries to Gemini.
43
+ // 10 entries × ~500 chars each = ~5000 chars per chunk,
44
+ // well within Gemini's 1M token context window.
45
+ // Keeping chunks small ensures consistent summarization quality.
46
+ const COMPACTION_CHUNK_SIZE = 10;
47
+
48
+ // Maximum entries to compact in a single tool call
49
+ // REVIEWER NOTE: Safety limit to prevent the tool from running
50
+ // for too long on a single invocation. If more entries need
51
+ // compacting, the tool can be called again.
52
+ const MAX_ENTRIES_PER_RUN = 100;
53
+
54
+ // ─── Type Guard ───────────────────────────────────────────────
55
+
56
+ export function isCompactLedgerArgs(
57
+ args: unknown
58
+ ): args is {
59
+ project?: string;
60
+ threshold?: number;
61
+ keep_recent?: number;
62
+ dry_run?: boolean;
63
+ } {
64
+ return typeof args === "object" && args !== null;
65
+ }
66
+
67
+ // ─── Gemini Summarization ─────────────────────────────────────
68
+
69
+ /**
70
+ * Summarize a batch of ledger entries using Gemini.
71
+ *
72
+ * REVIEWER NOTE: The prompt is carefully designed to preserve
73
+ * important information (decisions, file changes, error resolutions)
74
+ * while condensing narrative. The output format matches what we
75
+ * store in a rollup entry's summary field.
76
+ */
77
+ async function summarizeEntries(entries: any[]): Promise<string> {
78
+ if (!GOOGLE_API_KEY) {
79
+ throw new Error("Cannot compact ledger: GOOGLE_API_KEY required for Gemini summarization");
80
+ }
81
+
82
+ const genAI = new GoogleGenerativeAI(GOOGLE_API_KEY);
83
+ const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
84
+
85
+ // Build a concise representation of each entry for the prompt
86
+ const entriesText = entries.map((e, i) =>
87
+ `[${i + 1}] ${e.session_date || "unknown date"}: ${e.summary || "no summary"}\n` +
88
+ (e.decisions?.length ? ` Decisions: ${e.decisions.join("; ")}\n` : "") +
89
+ (e.files_changed?.length ? ` Files: ${e.files_changed.join(", ")}\n` : "")
90
+ ).join("\n");
91
+
92
+ const prompt =
93
+ `You are compressing a session history log. Summarize these ${entries.length} ` +
94
+ `work sessions into a single concise paragraph (max 500 words).\n\n` +
95
+ `PRESERVE: key decisions, important file changes, error resolutions, ` +
96
+ `architecture changes, and any recurring patterns.\n` +
97
+ `OMIT: routine operations, intermediate debugging steps, and redundant details.\n\n` +
98
+ `Sessions to summarize:\n${entriesText}\n\n` +
99
+ `Provide ONLY the summary paragraph, no headers or formatting.`;
100
+
101
+ // REVIEWER NOTE: Using truncation to prevent exceeding API limits.
102
+ // The prompt itself is structured to stay well within limits, but
103
+ // this is an extra safety net.
104
+ const truncatedPrompt = prompt.substring(0, 30000);
105
+
106
+ const result = await model.generateContent(truncatedPrompt);
107
+ const response = result.response;
108
+ return response.text();
109
+ }
110
+
111
+ // ─── Main Handler ─────────────────────────────────────────────
112
+
113
+ /**
114
+ * Compact old ledger entries into rollup summaries.
115
+ *
116
+ * REVIEWER NOTE: This handler can be called in two modes:
117
+ * 1. With a specific project → compact only that project
118
+ * 2. Without a project → auto-detect all candidates
119
+ * 3. With dry_run=true → preview what would be compacted
120
+ */
121
+ export async function compactLedgerHandler(args: unknown) {
122
+ if (!isCompactLedgerArgs(args)) {
123
+ throw new Error("Invalid arguments for session_compact_ledger");
124
+ }
125
+
126
+ const {
127
+ project,
128
+ threshold = 50,
129
+ keep_recent = 10,
130
+ dry_run = false,
131
+ } = args;
132
+
133
+ console.error(
134
+ `[compact_ledger] ${dry_run ? "DRY RUN: " : ""}` +
135
+ `project=${project || "auto-detect"}, threshold=${threshold}, keep_recent=${keep_recent}`
136
+ );
137
+
138
+ // Step 1: Find candidates
139
+ let candidates: any[];
140
+ if (project) {
141
+ // If specific project, check it directly
142
+ // v1.5.0: Scope direct query to user_id
143
+ const entries = await supabaseGet("session_ledger", {
144
+ project: `eq.${project}`,
145
+ user_id: `eq.${PRISM_USER_ID}`,
146
+ "archived_at": "is.null",
147
+ "is_rollup": "eq.false",
148
+ select: "id",
149
+ });
150
+ const count = Array.isArray(entries) ? entries.length : 0;
151
+ if (count <= threshold) {
152
+ return {
153
+ content: [{
154
+ type: "text",
155
+ text: `✅ Project "${project}" has ${count} active entries ` +
156
+ `(threshold: ${threshold}). No compaction needed.`,
157
+ }],
158
+ isError: false,
159
+ };
160
+ }
161
+ candidates = [{ project, total_entries: count, to_compact: count - keep_recent }];
162
+ } else {
163
+ // Auto-detect candidates using the RPC
164
+ // v1.5.0: Pass p_user_id for multi-tenant isolation
165
+ const result = await supabaseRpc("get_compaction_candidates", {
166
+ p_threshold: threshold,
167
+ p_keep_recent: keep_recent,
168
+ p_user_id: PRISM_USER_ID,
169
+ });
170
+ candidates = Array.isArray(result) ? result : [];
171
+ }
172
+
173
+ if (candidates.length === 0) {
174
+ return {
175
+ content: [{
176
+ type: "text",
177
+ text: `✅ No projects exceed the compaction threshold (${threshold} entries). ` +
178
+ `All clear!`,
179
+ }],
180
+ isError: false,
181
+ };
182
+ }
183
+
184
+ // Dry run: just report candidates
185
+ if (dry_run) {
186
+ const summary = candidates.map(c =>
187
+ `• ${c.project}: ${c.total_entries} entries (${c.to_compact} would be compacted)`
188
+ ).join("\n");
189
+
190
+ return {
191
+ content: [{
192
+ type: "text",
193
+ text: `🔍 Compaction preview (dry run):\n\n${summary}\n\n` +
194
+ `Run without dry_run to execute compaction.`,
195
+ }],
196
+ isError: false,
197
+ };
198
+ }
199
+
200
+ // Step 2: Compact each candidate project
201
+ const results: string[] = [];
202
+
203
+ for (const candidate of candidates) {
204
+ const proj = candidate.project;
205
+ const toCompact = Math.min(candidate.to_compact, MAX_ENTRIES_PER_RUN);
206
+
207
+ console.error(`[compact_ledger] Compacting ${toCompact} entries for "${proj}"`);
208
+
209
+ // Fetch oldest entries (the ones to be rolled up)
210
+ // v1.5.0: Scope to user_id
211
+ const oldEntries = await supabaseGet("session_ledger", {
212
+ project: `eq.${proj}`,
213
+ user_id: `eq.${PRISM_USER_ID}`,
214
+ "archived_at": "is.null",
215
+ "is_rollup": "eq.false",
216
+ order: "created_at.asc",
217
+ limit: String(toCompact),
218
+ select: "id,summary,decisions,files_changed,keywords,session_date",
219
+ });
220
+
221
+ if (!Array.isArray(oldEntries) || oldEntries.length === 0) {
222
+ results.push(`• ${proj}: no entries to compact`);
223
+ continue;
224
+ }
225
+
226
+ // Step 3: Chunked summarization
227
+ // REVIEWER NOTE: We chunk entries to avoid exceeding Gemini's
228
+ // token limits. Each chunk is summarized independently, then
229
+ // if multiple chunks exist, the sub-summaries are summarized again.
230
+ const chunks: any[][] = [];
231
+ for (let i = 0; i < oldEntries.length; i += COMPACTION_CHUNK_SIZE) {
232
+ chunks.push(oldEntries.slice(i, i + COMPACTION_CHUNK_SIZE));
233
+ }
234
+
235
+ let finalSummary: string;
236
+
237
+ if (chunks.length === 1) {
238
+ // Single chunk — summarize directly
239
+ finalSummary = await summarizeEntries(chunks[0]);
240
+ } else {
241
+ // Multiple chunks — recursive summarization
242
+ // First pass: summarize each chunk
243
+ const chunkSummaries = await Promise.all(
244
+ chunks.map(chunk => summarizeEntries(chunk))
245
+ );
246
+
247
+ // Second pass: summarize the summaries
248
+ // REVIEWER NOTE: This recursive approach handles arbitrarily
249
+ // large batches. In practice, MAX_ENTRIES_PER_RUN=100 with
250
+ // COMPACTION_CHUNK_SIZE=10 means at most 10 sub-summaries,
251
+ // which is well within Gemini's limits.
252
+ const metaEntries = chunkSummaries.map((s, i) => ({
253
+ session_date: `chunk ${i + 1}`,
254
+ summary: s,
255
+ decisions: [],
256
+ files_changed: [],
257
+ }));
258
+ finalSummary = await summarizeEntries(metaEntries);
259
+ }
260
+
261
+ // Collect all unique keywords from rolled-up entries
262
+ const allKeywords = [...new Set(
263
+ oldEntries.flatMap((e: any) => e.keywords || [])
264
+ )];
265
+
266
+ // Collect all unique files changed
267
+ const allFiles = [...new Set(
268
+ oldEntries.flatMap((e: any) => e.files_changed || [])
269
+ )];
270
+
271
+ // Step 4: Insert rollup entry
272
+ // v1.5.0: Include user_id in rollup entry
273
+ await supabasePost("session_ledger", {
274
+ project: proj,
275
+ user_id: PRISM_USER_ID,
276
+ summary: `[ROLLUP of ${oldEntries.length} sessions] ${finalSummary}`,
277
+ keywords: allKeywords,
278
+ files_changed: allFiles,
279
+ decisions: [`Rolled up ${oldEntries.length} sessions on ${new Date().toISOString()}`],
280
+ is_rollup: true,
281
+ rollup_count: oldEntries.length,
282
+ // REVIEWER NOTE: We need to provide required fields for the table.
283
+ // These are set to sensible defaults for rollup entries.
284
+ title: `Session Rollup (${oldEntries.length} entries)`,
285
+ agent_name: "prism-compactor",
286
+ conversation_id: `rollup-${Date.now()}`,
287
+ });
288
+
289
+ // Step 5: Archive old entries (soft-delete)
290
+ const entryIds = oldEntries.map((e: any) => e.id);
291
+ for (const id of entryIds) {
292
+ await supabasePatch(
293
+ "session_ledger",
294
+ { archived_at: new Date().toISOString() },
295
+ { id: `eq.${id}` }
296
+ );
297
+ }
298
+
299
+ results.push(
300
+ `• ${proj}: ${oldEntries.length} entries → 1 rollup ` +
301
+ `(${allKeywords.length} keywords preserved)`
302
+ );
303
+ }
304
+
305
+ return {
306
+ content: [{
307
+ type: "text",
308
+ text: `🧹 Ledger compaction complete:\n\n${results.join("\n")}\n\n` +
309
+ `Original entries are archived (soft-deleted), not permanently removed.`,
310
+ }],
311
+ isError: false,
312
+ };
313
+ }
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Tool Definitions (Schemas)
3
+ *
4
+ * This file defines the SHAPE of each tool — its name, description, and
5
+ * what input arguments it accepts. These definitions are sent to the AI client
6
+ * (e.g., Claude Desktop) so it knows what tools are available and how to call them.
7
+ *
8
+ * Each tool definition has:
9
+ * - name: Unique identifier used to route calls in server.ts
10
+ * - description: Human-readable text shown to the AI so it knows when to use the tool
11
+ * - inputSchema: JSON Schema describing the accepted arguments (types, required fields, defaults)
12
+ *
13
+ * The corresponding IMPLEMENTATIONS (what the tools actually do) are in handlers.ts.
14
+ *
15
+ * Tool Categories:
16
+ * 1. Search Tools — brave_web_search, brave_local_search
17
+ * 2. Code Mode Tools — brave_web_search_code_mode, brave_local_search_code_mode, code_mode_transform
18
+ * 3. AI Analysis Tools — brave_answers, gemini_research_paper_analysis
19
+ * 4. Session Memory — defined separately in sessionMemoryDefinitions.ts (optional)
20
+ *
21
+ * Adding a new tool:
22
+ * 1. Define it here (schema + type guard)
23
+ * 2. Implement the handler in handlers.ts
24
+ * 3. Export both from tools/index.ts
25
+ * 4. Add a case to the switch statement in server.ts
26
+ */
27
+
28
+ import { type Tool } from "@modelcontextprotocol/sdk/types.js";
29
+
30
+ // ─── Search Tools ─────────────────────────────────────────────
31
+
32
+ // Code Mode: Search + JavaScript extraction
33
+ // The "code mode" pattern works like this:
34
+ // 1. Perform a regular Brave search to get raw API JSON
35
+ // 2. Run user-provided JavaScript code against that JSON in a QuickJS sandbox
36
+ // 3. Return only the extracted/transformed output (much smaller than the full response)
37
+ // This dramatically reduces token usage when the AI only needs specific fields from search results.
38
+
39
+ export const BRAVE_WEB_SEARCH_CODE_MODE_TOOL: Tool = {
40
+ name: "brave_web_search_code_mode",
41
+ description:
42
+ "Performs a web search using the Brave Search API, and then runs a custom JavaScript code string against the RAW API RESPONSE in a secure QuickJS sandbox. " +
43
+ "This drastically reduces context window usage by only returning the output of your script. " +
44
+ "Use this for broad information gathering, recent events, or when you need diverse web sources and only need specific parts of the result. " +
45
+ "Your script should read the 'DATA' global variable (a JSON string of the API response), process it, and use console.log() to print the desired output.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ query: {
50
+ type: "string",
51
+ description: "Search query (max 400 chars, 50 words)",
52
+ },
53
+ count: {
54
+ type: "number",
55
+ description: "Number of results (1-20, default 10)",
56
+ default: 10,
57
+ },
58
+ offset: {
59
+ type: "number",
60
+ description: "Pagination offset (max 9, default 0)",
61
+ default: 0,
62
+ },
63
+ code: {
64
+ type: "string",
65
+ description: "JavaScript code to execute against the 'DATA' variable. E.g. `const r = JSON.parse(DATA); console.log(r.web.results.map(x => x.title).join(', '));`",
66
+ },
67
+ language: {
68
+ type: "string",
69
+ description: "Language of the code. Only 'javascript' is supported.",
70
+ default: "javascript",
71
+ }
72
+ },
73
+ required: ["query", "code"],
74
+ },
75
+ };
76
+
77
+ // Standard web search — returns formatted text results (title, description, URL).
78
+ // This is the simplest search tool. For extracting specific fields, use the code mode variant above.
79
+ export const WEB_SEARCH_TOOL: Tool = {
80
+ name: "brave_web_search",
81
+ description:
82
+ "Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. " +
83
+ "Use this for broad information gathering, recent events, or when you need diverse web sources. " +
84
+ "Supports pagination, content filtering, and freshness controls. " +
85
+ "Maximum 20 results per request, with offset for pagination. ",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ query: {
90
+ type: "string",
91
+ description: "Search query (max 400 chars, 50 words)",
92
+ },
93
+ count: {
94
+ type: "number",
95
+ description: "Number of results (1-20, default 10)",
96
+ default: 10,
97
+ },
98
+ offset: {
99
+ type: "number",
100
+ description: "Pagination offset (max 9, default 0)",
101
+ default: 0,
102
+ },
103
+ },
104
+ required: ["query"],
105
+ },
106
+ };
107
+
108
+ // ─── Local/Business Search Tools ──────────────────────────────
109
+
110
+ // Searches for physical businesses and places (restaurants, stores, services).
111
+ // Returns structured data: name, address, phone, rating, hours, price range.
112
+ // If no local results are found, automatically falls back to a regular web search.
113
+ export const LOCAL_SEARCH_TOOL: Tool = {
114
+ name: "brave_local_search",
115
+ description:
116
+ "Searches for local businesses and places using Brave's Local Search API. " +
117
+ "Best for queries related to physical locations, businesses, restaurants, services, etc. " +
118
+ "Returns detailed information including:\n" +
119
+ "- Business names and addresses\n" +
120
+ "- Ratings and review counts\n" +
121
+ "- Phone numbers and opening hours\n" +
122
+ "Use this when the query implies 'near me' or mentions specific locations. " +
123
+ "Automatically falls back to web search if no local results are found.",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: {
127
+ query: {
128
+ type: "string",
129
+ description: "Local search query (e.g. 'pizza near Central Park')",
130
+ },
131
+ count: {
132
+ type: "number",
133
+ description: "Number of results (1-20, default 5)",
134
+ default: 5,
135
+ },
136
+ },
137
+ required: ["query"],
138
+ },
139
+ };
140
+
141
+ // Code mode variant for local search — same pattern as web search code mode.
142
+ // Useful when you only need specific fields from the detailed POI (Point of Interest) data.
143
+ export const BRAVE_LOCAL_SEARCH_CODE_MODE_TOOL: Tool = {
144
+ name: "brave_local_search_code_mode",
145
+ description:
146
+ "Performs a local search using Brave APIs, and then runs a custom JavaScript code string against the RAW API RESPONSE in a secure QuickJS sandbox. " +
147
+ "This reduces context window usage by only returning the output of your script. " +
148
+ "Use this for local/business lookups when you only need specific fields from large local payloads. " +
149
+ "Your script should read the 'DATA' global variable (a JSON string payload) and use console.log() to print the desired output.",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: {
153
+ query: {
154
+ type: "string",
155
+ description: "Local search query (e.g. 'pizza near Central Park')",
156
+ },
157
+ count: {
158
+ type: "number",
159
+ description: "Number of results (1-20, default 5)",
160
+ default: 5,
161
+ },
162
+ code: {
163
+ type: "string",
164
+ description: "JavaScript code to execute against the 'DATA' variable.",
165
+ },
166
+ language: {
167
+ type: "string",
168
+ description: "Language of the code. Only 'javascript' is supported.",
169
+ default: "javascript",
170
+ },
171
+ },
172
+ required: ["query", "code"],
173
+ },
174
+ };
175
+
176
+ // ─── Universal Transform Tool ─────────────────────────────────
177
+
178
+ // This is NOT tied to Brave Search — it works with output from ANY MCP tool.
179
+ // Pass it raw data from any source + a JavaScript extraction script,
180
+ // and it returns only the fields you need. Great for reducing token usage.
181
+ export const CODE_MODE_TRANSFORM_TOOL: Tool = {
182
+ name: "code_mode_transform",
183
+ description:
184
+ "A universal code-mode transformer. Takes RAW TEXT or JSON output from ANY MCP tool (GitHub, Firecrawl, chrome-devtools, camoufox, codegraphcontext, videoMcp, arxiv, etc.) " +
185
+ "and runs a custom JavaScript code string against it in a secure QuickJS sandbox. " +
186
+ "Use this as a second step after calling any tool that returns large payloads — pass the raw output as 'data' and a JS extraction script as 'code'. " +
187
+ "Your script reads the 'DATA' global variable (a string of the tool output) and uses console.log() to print only the fields you need. " +
188
+ "Typical use cases: extract only issue titles/IDs from GitHub list_issues, pull specific selectors from DOM snapshots, summarize crawl results, extract timestamps from video transcripts.",
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: {
192
+ data: {
193
+ type: "string",
194
+ description: "The raw text or JSON output from another MCP tool to process.",
195
+ },
196
+ code: {
197
+ type: "string",
198
+ description:
199
+ "JavaScript code to execute. The 'DATA' global variable contains the raw data string. Use console.log() to output your extraction. " +
200
+ "Example: `var d = JSON.parse(DATA); console.log(d.items.map(function(i){return i.title}).join('\\n'));`",
201
+ },
202
+ language: {
203
+ type: "string",
204
+ description: "Language of the code. Only 'javascript' is supported.",
205
+ default: "javascript",
206
+ },
207
+ source_tool: {
208
+ type: "string",
209
+ description: "Optional. Name of the MCP tool that produced the data (for logging/metrics only).",
210
+ },
211
+ },
212
+ required: ["data", "code"],
213
+ },
214
+ };
215
+
216
+ // ─── AI Analysis Tools ────────────────────────────────────────
217
+
218
+ // AI-grounded answers — uses Brave's AI grounding endpoint (OpenAI-compatible API).
219
+ // Returns concise, web-grounded answers rather than raw search results.
220
+ // Requires a separate BRAVE_ANSWERS_API_KEY.
221
+ export const BRAVE_ANSWERS_TOOL: Tool = {
222
+ name: "brave_answers",
223
+ description:
224
+ "Returns direct AI answers grounded in Brave Search using Brave AI Grounding. " +
225
+ "Uses an OpenAI-compatible chat completions endpoint and is best for concise answer generation with live web grounding.",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ query: {
230
+ type: "string",
231
+ description: "Question or prompt to answer",
232
+ },
233
+ model: {
234
+ type: "string",
235
+ description: "Model name for Brave AI Grounding (default: brave)",
236
+ default: "brave",
237
+ },
238
+ },
239
+ required: ["query"],
240
+ },
241
+ };
242
+
243
+ // Analyzes academic research papers using Google's Gemini model.
244
+ // Supports multiple analysis types: summary, critique, literature review, key findings.
245
+ // Requires GOOGLE_API_KEY to be configured.
246
+ export const RESEARCH_PAPER_ANALYSIS_TOOL: Tool = {
247
+ name: "gemini_research_paper_analysis",
248
+ description:
249
+ "Performs in-depth analysis of research papers using Google's Gemini-2.0-flash model. " +
250
+ "Ideal for academic research, literature reviews, and deep understanding of scientific papers. " +
251
+ "Can extract key findings, provide critical evaluation, summarize complex research, " +
252
+ "and place papers within the broader research landscape. " +
253
+ "Best for long-form academic content that requires expert analysis.",
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {
257
+ paperContent: {
258
+ type: "string",
259
+ description: "The full text of the research paper to analyze",
260
+ },
261
+ analysisType: {
262
+ type: "string",
263
+ description: "Type of analysis to perform (summary, critique, literature review, key findings, or comprehensive)",
264
+ enum: ["summary", "critique", "literature review", "key findings", "comprehensive"],
265
+ default: "comprehensive",
266
+ },
267
+ additionalContext: {
268
+ type: "string",
269
+ description: "Optional additional context or specific questions to guide the analysis",
270
+ },
271
+ },
272
+ required: ["paperContent"],
273
+ },
274
+ };
275
+
276
+ // ─── Type Guards ──────────────────────────────────────────────
277
+ //
278
+ // Type guards validate that incoming tool arguments match the expected shape.
279
+ // They are used by handlers to safely access argument properties without
280
+ // runtime errors. Each guard checks that required fields exist and are
281
+ // the correct type.
282
+ //
283
+ // Pattern: if (!isMyToolArgs(args)) throw new Error("Invalid arguments");
284
+
285
+ /** Validates arguments for brave_web_search */
286
+ export function isBraveWebSearchArgs(
287
+ args: unknown
288
+ ): args is { query: string; count?: number; offset?: number } {
289
+ return (
290
+ typeof args === "object" &&
291
+ args !== null &&
292
+ "query" in args &&
293
+ typeof (args as { query: string }).query === "string"
294
+ );
295
+ }
296
+
297
+ export function isBraveLocalSearchArgs(
298
+ args: unknown
299
+ ): args is { query: string; count?: number } {
300
+ return (
301
+ typeof args === "object" &&
302
+ args !== null &&
303
+ "query" in args &&
304
+ typeof (args as { query: string }).query === "string"
305
+ );
306
+ }
307
+
308
+ export function isBraveAnswersArgs(
309
+ args: unknown
310
+ ): args is { query: string; model?: string } {
311
+ return (
312
+ typeof args === "object" &&
313
+ args !== null &&
314
+ "query" in args &&
315
+ typeof (args as { query: string }).query === "string"
316
+ );
317
+ }
318
+
319
+ export function isGeminiResearchPaperAnalysisArgs(
320
+ args: unknown
321
+ ): args is { paperContent: string; analysisType?: string; additionalContext?: string } {
322
+ return (
323
+ typeof args === "object" &&
324
+ args !== null &&
325
+ "paperContent" in args &&
326
+ typeof (args as { paperContent: string }).paperContent === "string"
327
+ );
328
+ }
329
+
330
+ export function isBraveWebSearchCodeModeArgs(
331
+ args: unknown
332
+ ): args is { query: string; count?: number; offset?: number; code: string; language?: string } {
333
+ return (
334
+ typeof args === "object" &&
335
+ args !== null &&
336
+ "query" in args &&
337
+ typeof (args as { query: string }).query === "string" &&
338
+ "code" in args &&
339
+ typeof (args as { code: string }).code === "string"
340
+ );
341
+ }
342
+
343
+ export function isBraveLocalSearchCodeModeArgs(
344
+ args: unknown
345
+ ): args is { query: string; count?: number; code: string; language?: string } {
346
+ return (
347
+ typeof args === "object" &&
348
+ args !== null &&
349
+ "query" in args &&
350
+ typeof (args as { query: string }).query === "string" &&
351
+ "code" in args &&
352
+ typeof (args as { code: string }).code === "string"
353
+ );
354
+ }
355
+
356
+ export function isCodeModeTransformArgs(
357
+ args: unknown
358
+ ): args is { data: string; code: string; language?: string; source_tool?: string } {
359
+ return (
360
+ typeof args === "object" &&
361
+ args !== null &&
362
+ "data" in args &&
363
+ typeof (args as { data: string }).data === "string" &&
364
+ "code" in args &&
365
+ typeof (args as { code: string }).code === "string"
366
+ );
367
+ }