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.
- package/.gitmodules +3 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +970 -0
- package/benchmark.ts +172 -0
- package/call_chrome_mcp.py +96 -0
- package/docker-compose.yml +67 -0
- package/execute_via_chrome_mcp.py +133 -0
- package/gmail_auth_test.py +29 -0
- package/gmail_list_latest_5.py +27 -0
- package/index.ts +34 -0
- package/list_chrome_tools.py +70 -0
- package/package.json +64 -0
- package/patch_cgc_mcp.py +90 -0
- package/repomix-output.xml +9 -0
- package/run_server.sh +9 -0
- package/server.json +78 -0
- package/src/config.ts +85 -0
- package/src/server.ts +627 -0
- package/src/tools/compactionHandler.ts +313 -0
- package/src/tools/definitions.ts +367 -0
- package/src/tools/handlers.ts +261 -0
- package/src/tools/index.ts +38 -0
- package/src/tools/sessionMemoryDefinitions.ts +437 -0
- package/src/tools/sessionMemoryHandlers.ts +774 -0
- package/src/utils/braveApi.ts +375 -0
- package/src/utils/embeddingApi.ts +97 -0
- package/src/utils/executor.ts +105 -0
- package/src/utils/googleAi.ts +107 -0
- package/src/utils/keywordExtractor.ts +207 -0
- package/src/utils/supabaseApi.ts +194 -0
- package/supabase/migrations/015_session_memory.sql +145 -0
- package/supabase/migrations/016_knowledge_accumulation.sql +315 -0
- package/supabase/migrations/017_ledger_compaction.sql +74 -0
- package/supabase/migrations/018_semantic_search.sql +110 -0
- package/supabase/migrations/019_concurrency_control.sql +320 -0
- package/supabase/migrations/020_multi_tenant_rls.sql +459 -0
- package/test_cross_mcp.js +393 -0
- package/test_mcp_schema.js +83 -0
- package/tests/test_knowledge_system.js +319 -0
- package/tsconfig.json +16 -0
- package/vertex-ai/test_claude_vertex.py +78 -0
- package/vertex-ai/test_gemini_vertex.py +39 -0
- package/vertex-ai/test_hybrid_search_pipeline.ts +296 -0
- package/vertex-ai/test_pipeline_benchmark.ts +251 -0
- package/vertex-ai/test_realworld_comparison.ts +290 -0
- 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
|
+
}
|