trapic-mcp 0.1.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/bin/trapic-mcp.mjs +902 -0
  4. package/bin/wrapper.sh +5 -0
  5. package/dist/archive.d.ts +7 -0
  6. package/dist/archive.js +116 -0
  7. package/dist/audit.d.ts +5 -0
  8. package/dist/audit.js +16 -0
  9. package/dist/background.d.ts +8 -0
  10. package/dist/background.js +17 -0
  11. package/dist/config.d.ts +46 -0
  12. package/dist/config.js +20 -0
  13. package/dist/conflict.d.ts +14 -0
  14. package/dist/conflict.js +103 -0
  15. package/dist/embedding.d.ts +6 -0
  16. package/dist/embedding.js +74 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +104 -0
  19. package/dist/llm.d.ts +10 -0
  20. package/dist/llm.js +47 -0
  21. package/dist/ollama.d.ts +11 -0
  22. package/dist/ollama.js +63 -0
  23. package/dist/quota.d.ts +7 -0
  24. package/dist/quota.js +16 -0
  25. package/dist/rate-limit.d.ts +5 -0
  26. package/dist/rate-limit.js +38 -0
  27. package/dist/request-context.d.ts +3 -0
  28. package/dist/request-context.js +12 -0
  29. package/dist/supabase.d.ts +2 -0
  30. package/dist/supabase.js +16 -0
  31. package/dist/team-access.d.ts +5 -0
  32. package/dist/team-access.js +35 -0
  33. package/dist/tools/active.d.ts +2 -0
  34. package/dist/tools/active.js +63 -0
  35. package/dist/tools/assert.d.ts +3 -0
  36. package/dist/tools/assert.js +141 -0
  37. package/dist/tools/chain.d.ts +2 -0
  38. package/dist/tools/chain.js +118 -0
  39. package/dist/tools/context.d.ts +7 -0
  40. package/dist/tools/context.js +270 -0
  41. package/dist/tools/create.d.ts +2 -0
  42. package/dist/tools/create.js +126 -0
  43. package/dist/tools/extract.d.ts +2 -0
  44. package/dist/tools/extract.js +95 -0
  45. package/dist/tools/preload.d.ts +10 -0
  46. package/dist/tools/preload.js +112 -0
  47. package/dist/tools/search.d.ts +2 -0
  48. package/dist/tools/search.js +92 -0
  49. package/dist/tools/summary.d.ts +2 -0
  50. package/dist/tools/summary.js +176 -0
  51. package/dist/tools/update.d.ts +2 -0
  52. package/dist/tools/update.js +134 -0
  53. package/dist/worker.d.ts +15 -0
  54. package/dist/worker.js +700 -0
  55. package/package.json +59 -0
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+ import { getSupabase } from "../supabase.js";
3
+ import { generateEmbedding } from "../embedding.js";
4
+ import { audit } from "../audit.js";
5
+ import { getVisibleAuthors } from "../team-access.js";
6
+ export function registerSearch(server, userId) {
7
+ server.tool("trapic_search", "Semantic search for traces using natural language query. Combines vector similarity with optional tag/scope filtering. " +
8
+ "使用自然語言進行語意搜尋,結合向量相似度與標籤/範圍過濾。", {
9
+ query: z.string().describe("Natural language search query. " +
10
+ "自然語言搜尋查詢"),
11
+ tags: z.array(z.string()).optional().describe("Filter by tags (returns traces matching ANY of these tags). " +
12
+ "按標籤過濾(符合任一標籤即返回)"),
13
+ scope: z.enum(["personal", "team", "org"]).optional().describe("Filter by visibility scope. " +
14
+ "按可見範圍過濾"),
15
+ status: z.enum(["active", "superseded", "deprecated"]).default("active").describe("Filter by status (default: active). " +
16
+ "按狀態過濾(預設:active)"),
17
+ limit: z.number().int().min(1).max(50).default(10).describe("Maximum number of results (1-50, default: 10). " +
18
+ "最多返回筆數(1-50,預設 10)"),
19
+ threshold: z.number().min(0).max(1).default(0.7).describe("Minimum similarity threshold (0-1, default: 0.7). " +
20
+ "最低相似度閾值(0-1,預設 0.7)"),
21
+ }, async (params) => {
22
+ try {
23
+ const supabase = getSupabase();
24
+ // Generate query embedding
25
+ const queryEmbedding = await generateEmbedding(params.query);
26
+ // Call the search_traces RPC
27
+ const { data, error } = await supabase.rpc("search_traces", {
28
+ query_embedding: queryEmbedding,
29
+ filter_tags: params.tags ?? [],
30
+ filter_scope: params.scope ?? null,
31
+ filter_status: params.status,
32
+ match_limit: params.limit,
33
+ match_threshold: params.threshold,
34
+ });
35
+ if (error) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: `Error searching traces: ${error.message}\n搜尋 Trace 失敗:${error.message}`,
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ // Post-filter: show own traces + team members' traces
46
+ const visibleAuthors = userId ? await getVisibleAuthors(userId) : null;
47
+ const filtered = visibleAuthors
48
+ ? (data ?? []).filter((row) => visibleAuthors.has(row.author))
49
+ : (data ?? []);
50
+ const results = filtered.map((row) => ({
51
+ id: row.id,
52
+ claim: row.claim,
53
+ reason: row.reason,
54
+ status: row.status,
55
+ tags: row.tags,
56
+ caused_by: row.caused_by,
57
+ confidence: row.confidence,
58
+ source: row.source,
59
+ author: row.author,
60
+ created_at: row.created_at,
61
+ similarity: typeof row.similarity === "number"
62
+ ? Math.round(row.similarity * 1000) / 1000
63
+ : row.similarity,
64
+ }));
65
+ if (userId)
66
+ audit(userId, "trace.search", "trace", undefined, { query: params.query, results: results.length });
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: JSON.stringify({
72
+ query: params.query,
73
+ result_count: results.length,
74
+ results,
75
+ }, null, 2),
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ catch (err) {
81
+ const message = err instanceof Error ? err.message : String(err);
82
+ return {
83
+ content: [
84
+ {
85
+ type: "text",
86
+ text: `Error: ${message}`,
87
+ },
88
+ ],
89
+ };
90
+ }
91
+ });
92
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerSummary(server: McpServer, userId: string | null): void;
@@ -0,0 +1,176 @@
1
+ import { z } from "zod";
2
+ import { getSupabase } from "../supabase.js";
3
+ import { generateEmbeddings } from "../embedding.js";
4
+ import { contextualizeTrace } from "./context.js";
5
+ import { llmChat } from "../llm.js";
6
+ import { checkMonthlyQuota } from "../quota.js";
7
+ const SUMMARY_EXTRACTION_PROMPT = `You are a knowledge extraction engine. Given a conversation summary, extract all key decisions, discoveries, and conclusions as Traces.
8
+
9
+ A Trace is a minimal causal proposition — one clear statement (claim) with an optional reason (why).
10
+
11
+ ## What to extract
12
+
13
+ 1. **Decisions** — what was decided and why
14
+ 2. **Discoveries** — what was learned or realized
15
+ 3. **Conclusions** — what was concluded from the discussion
16
+
17
+ ## Rules
18
+
19
+ - Each trace = ONE proposition. Do not combine multiple ideas.
20
+ - claim = what happened / what was concluded (one clear sentence)
21
+ - reason = why (optional — some facts have no reason)
22
+ - Auto-assign tags based on content (e.g., "architecture", "design", "api", "performance", "security", "ux", "database", "deployment", "process", "decision")
23
+ - Be thorough but avoid extracting trivial or obvious statements
24
+
25
+ ## Output format
26
+
27
+ Return JSON: { "traces": [{ "claim": "...", "reason": "..." or null, "tags": ["..."] }] }`;
28
+ export function registerSummary(server, userId) {
29
+ server.tool("trapic_auto_summary", "Extract and create multiple traces from a conversation summary in one call. " +
30
+ "Uses LLM to identify key decisions, discoveries, and conclusions, then writes them all to DB. " +
31
+ "從對話摘要中批次提取並建立多條 trace。", {
32
+ text: z.string().describe("Conversation summary text to extract traces from. 要提取 trace 的對話摘要文字"),
33
+ tags: z.array(z.string()).default([]).describe("Additional tags to apply to all created traces. 額外標籤(套用到所有建立的 trace)"),
34
+ source_id: z.string().optional().describe("Source reference ID (e.g. session ID). 來源參考 ID"),
35
+ }, async (params) => {
36
+ try {
37
+ if (!userId) {
38
+ return {
39
+ content: [{ type: "text", text: "Error: Authentication required." }],
40
+ };
41
+ }
42
+ const effectiveUserId = userId;
43
+ // Check monthly quota before doing LLM work
44
+ const quota = await checkMonthlyQuota(effectiveUserId);
45
+ if (!quota.allowed) {
46
+ return {
47
+ content: [{
48
+ type: "text",
49
+ text: `Monthly trace limit reached (${quota.used}/${quota.limit}). Resets next month.`,
50
+ }],
51
+ };
52
+ }
53
+ // 1. Use LLM to extract traces from summary
54
+ const content = await llmChat([
55
+ { role: "system", content: SUMMARY_EXTRACTION_PROMPT },
56
+ { role: "user", content: params.text },
57
+ ]);
58
+ let extracted;
59
+ try {
60
+ const parsed = JSON.parse(content);
61
+ const rawTraces = Array.isArray(parsed) ? parsed : (parsed.traces ?? parsed.results ?? []);
62
+ extracted = rawTraces
63
+ .map((t) => ({
64
+ claim: String(t.claim ?? ""),
65
+ reason: t.reason ? String(t.reason) : null,
66
+ tags: Array.isArray(t.tags) ? t.tags.map(String) : [],
67
+ }))
68
+ .filter((t) => t.claim.trim().length > 0);
69
+ }
70
+ catch {
71
+ return {
72
+ content: [{
73
+ type: "text",
74
+ text: `Error parsing LLM response. Raw output:\n${content}\n\nLLM 回應解析失敗。`,
75
+ }],
76
+ };
77
+ }
78
+ // Cap at 20 to prevent excessive insertions
79
+ extracted = extracted.slice(0, 20);
80
+ if (extracted.length === 0) {
81
+ return {
82
+ content: [{
83
+ type: "text",
84
+ text: JSON.stringify({
85
+ message: "No traces extracted from summary. 摘要中未提取到任何 trace。",
86
+ traces_created: 0,
87
+ }, null, 2),
88
+ }],
89
+ };
90
+ }
91
+ // 2. Truncate to remaining quota (use quota from initial check)
92
+ extracted = extracted.slice(0, Math.min(20, quota.remaining));
93
+ // 3. Batch generate embeddings, then create traces in DB
94
+ const supabase = getSupabase();
95
+ const created = [];
96
+ const errors = [];
97
+ // Batch embedding generation
98
+ const embeddingTexts = extracted.map((trace) => trace.reason ? `${trace.claim} ${trace.reason}` : trace.claim);
99
+ let embeddings;
100
+ try {
101
+ embeddings = await generateEmbeddings(embeddingTexts);
102
+ }
103
+ catch (embErr) {
104
+ const embMsg = embErr instanceof Error ? embErr.message : String(embErr);
105
+ return {
106
+ content: [{ type: "text", text: `Error generating embeddings: ${embMsg}` }],
107
+ };
108
+ }
109
+ // Insert traces in batches of 5
110
+ const BATCH_SIZE = 5;
111
+ for (let i = 0; i < extracted.length; i += BATCH_SIZE) {
112
+ const batch = extracted.slice(i, i + BATCH_SIZE);
113
+ const batchEmbeddings = embeddings.slice(i, i + BATCH_SIZE);
114
+ const results = await Promise.allSettled(batch.map(async (trace, idx) => {
115
+ // Merge user-provided tags with LLM-extracted tags
116
+ const mergedTags = [...new Set([...params.tags, ...trace.tags])];
117
+ // Insert into DB
118
+ const { data, error } = await supabase
119
+ .from("traces")
120
+ .insert({
121
+ claim: trace.claim,
122
+ reason: trace.reason,
123
+ scope: "personal",
124
+ author: effectiveUserId,
125
+ tags: mergedTags,
126
+ caused_by: [],
127
+ source: "extraction",
128
+ source_id: params.source_id ?? null,
129
+ confidence: "low",
130
+ references: [],
131
+ embedding: batchEmbeddings[idx],
132
+ })
133
+ .select()
134
+ .single();
135
+ if (error) {
136
+ throw new Error(`Failed to create trace "${trace.claim}": ${error.message}`);
137
+ }
138
+ // Contextualize (non-blocking failure)
139
+ await contextualizeTrace(data.id).catch(() => null);
140
+ return {
141
+ id: data.id,
142
+ claim: data.claim,
143
+ reason: data.reason,
144
+ tags: data.tags,
145
+ };
146
+ }));
147
+ for (const result of results) {
148
+ if (result.status === "fulfilled") {
149
+ created.push(result.value);
150
+ }
151
+ else {
152
+ errors.push(result.reason?.message ?? String(result.reason));
153
+ }
154
+ }
155
+ }
156
+ return {
157
+ content: [{
158
+ type: "text",
159
+ text: JSON.stringify({
160
+ message: `Created ${created.length} trace(s) from summary. 從摘要建立了 ${created.length} 條 trace。`,
161
+ traces_created: created.length,
162
+ traces_extracted: extracted.length,
163
+ traces: created,
164
+ errors: errors.length > 0 ? errors : undefined,
165
+ }, null, 2),
166
+ }],
167
+ };
168
+ }
169
+ catch (err) {
170
+ const message = err instanceof Error ? err.message : String(err);
171
+ return {
172
+ content: [{ type: "text", text: `Error: ${message}` }],
173
+ };
174
+ }
175
+ });
176
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerUpdate(server: McpServer, userId: string | null): void;
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+ import { getSupabase } from "../supabase.js";
3
+ import { generateEmbedding } from "../embedding.js";
4
+ import { audit } from "../audit.js";
5
+ export function registerUpdate(server, userId) {
6
+ server.tool("trapic_update", "Update an existing trace — change status, tags, claim/reason, or mark as superseded. " +
7
+ "更新現有 trace — 可以變更狀態、標籤、claim/reason,或標記為被取代。", {
8
+ trace_id: z.string().uuid().describe("ID of the trace to update. " +
9
+ "要更新的 trace ID"),
10
+ claim: z.string().optional().describe("Updated claim text. Will regenerate embedding if changed. " +
11
+ "更新的 claim 文字。如果變更會重新產生 embedding"),
12
+ reason: z.string().optional().describe("Updated reason text. Will regenerate embedding if changed. " +
13
+ "更新的 reason 文字。如果變更會重新產生 embedding"),
14
+ status: z.enum(["active", "superseded", "deprecated"]).optional().describe("New status for the trace. " +
15
+ "trace 的新狀態"),
16
+ superseded_by: z.string().uuid().optional().describe("ID of the trace that supersedes this one (sets status to 'superseded' automatically). " +
17
+ "取代此 trace 的新 trace ID(會自動將狀態設為 superseded)"),
18
+ tags: z.array(z.string()).optional().describe("Replace tags with this new list. " +
19
+ "用新的標籤列表替換"),
20
+ confidence: z.enum(["high", "medium", "low"]).optional().describe("Updated confidence level. " +
21
+ "更新的信心程度"),
22
+ references: z.array(z.string()).optional().describe("Replace references with this new list. " +
23
+ "用新的參考連結列表替換"),
24
+ }, async (params) => {
25
+ try {
26
+ const supabase = getSupabase();
27
+ // Build update object with only provided fields
28
+ const update = {};
29
+ if (params.claim !== undefined)
30
+ update.claim = params.claim;
31
+ if (params.reason !== undefined)
32
+ update.reason = params.reason;
33
+ if (params.tags !== undefined)
34
+ update.tags = params.tags;
35
+ if (params.confidence !== undefined)
36
+ update.confidence = params.confidence;
37
+ if (params.references !== undefined)
38
+ update.references = params.references;
39
+ // Handle superseded_by — auto-set status
40
+ if (params.superseded_by !== undefined) {
41
+ update.superseded_by = params.superseded_by;
42
+ update.status = "superseded";
43
+ }
44
+ else if (params.status !== undefined) {
45
+ update.status = params.status;
46
+ }
47
+ // Regenerate embedding if claim or reason changed
48
+ if (params.claim !== undefined || params.reason !== undefined) {
49
+ // Fetch current trace to get existing claim/reason if only one changed
50
+ let query = supabase
51
+ .from("traces")
52
+ .select("claim, reason")
53
+ .eq("id", params.trace_id);
54
+ if (userId)
55
+ query = query.eq("author", userId);
56
+ const { data: current, error: fetchError } = await query.single();
57
+ if (fetchError) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text",
62
+ text: `Error fetching current trace: ${fetchError.message}\n取得現有 trace 失敗:${fetchError.message}`,
63
+ },
64
+ ],
65
+ };
66
+ }
67
+ const newClaim = params.claim ?? current.claim;
68
+ const newReason = params.reason ?? current.reason;
69
+ const embeddingText = newReason
70
+ ? `${newClaim} ${newReason}`
71
+ : newClaim;
72
+ update.embedding = await generateEmbedding(embeddingText);
73
+ }
74
+ if (Object.keys(update).length === 0) {
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text: "No fields to update. Provide at least one field to change.\n沒有要更新的欄位,請至少提供一個要變更的欄位。",
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ let updateQuery = supabase
85
+ .from("traces")
86
+ .update(update)
87
+ .eq("id", params.trace_id);
88
+ if (userId)
89
+ updateQuery = updateQuery.eq("author", userId);
90
+ const { data, error } = await updateQuery
91
+ .select()
92
+ .single();
93
+ if (error) {
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: `Error updating trace: ${error.message}\n更新 trace 失敗:${error.message}`,
99
+ },
100
+ ],
101
+ };
102
+ }
103
+ if (userId)
104
+ audit(userId, "trace.update", "trace", params.trace_id);
105
+ return {
106
+ content: [
107
+ {
108
+ type: "text",
109
+ text: JSON.stringify({
110
+ success: true,
111
+ message: "Trace updated successfully / Trace 更新成功",
112
+ trace: {
113
+ id: data.id,
114
+ claim: data.claim,
115
+ reason: data.reason,
116
+ status: data.status,
117
+ superseded_by: data.superseded_by,
118
+ tags: data.tags,
119
+ confidence: data.confidence,
120
+ updated_at: data.updated_at,
121
+ },
122
+ }, null, 2),
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ catch (err) {
128
+ const message = err instanceof Error ? err.message : String(err);
129
+ return {
130
+ content: [{ type: "text", text: `Error: ${message}` }],
131
+ };
132
+ }
133
+ });
134
+ }
@@ -0,0 +1,15 @@
1
+ export interface Env {
2
+ SUPABASE_URL: string;
3
+ SUPABASE_SERVICE_ROLE_KEY: string;
4
+ OPENAI_API_KEY: string;
5
+ OPENAI_EMBED_MODEL?: string;
6
+ GROQ_API_KEY?: string;
7
+ GROQ_CHAT_MODEL?: string;
8
+ ARCHIVE_BUCKET: R2Bucket;
9
+ RESEND_API_KEY?: string;
10
+ }
11
+ declare const _default: {
12
+ fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
13
+ scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void>;
14
+ };
15
+ export default _default;