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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/trapic-mcp.mjs +902 -0
- package/bin/wrapper.sh +5 -0
- package/dist/archive.d.ts +7 -0
- package/dist/archive.js +116 -0
- package/dist/audit.d.ts +5 -0
- package/dist/audit.js +16 -0
- package/dist/background.d.ts +8 -0
- package/dist/background.js +17 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.js +20 -0
- package/dist/conflict.d.ts +14 -0
- package/dist/conflict.js +103 -0
- package/dist/embedding.d.ts +6 -0
- package/dist/embedding.js +74 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +104 -0
- package/dist/llm.d.ts +10 -0
- package/dist/llm.js +47 -0
- package/dist/ollama.d.ts +11 -0
- package/dist/ollama.js +63 -0
- package/dist/quota.d.ts +7 -0
- package/dist/quota.js +16 -0
- package/dist/rate-limit.d.ts +5 -0
- package/dist/rate-limit.js +38 -0
- package/dist/request-context.d.ts +3 -0
- package/dist/request-context.js +12 -0
- package/dist/supabase.d.ts +2 -0
- package/dist/supabase.js +16 -0
- package/dist/team-access.d.ts +5 -0
- package/dist/team-access.js +35 -0
- package/dist/tools/active.d.ts +2 -0
- package/dist/tools/active.js +63 -0
- package/dist/tools/assert.d.ts +3 -0
- package/dist/tools/assert.js +141 -0
- package/dist/tools/chain.d.ts +2 -0
- package/dist/tools/chain.js +118 -0
- package/dist/tools/context.d.ts +7 -0
- package/dist/tools/context.js +270 -0
- package/dist/tools/create.d.ts +2 -0
- package/dist/tools/create.js +126 -0
- package/dist/tools/extract.d.ts +2 -0
- package/dist/tools/extract.js +95 -0
- package/dist/tools/preload.d.ts +10 -0
- package/dist/tools/preload.js +112 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +92 -0
- package/dist/tools/summary.d.ts +2 -0
- package/dist/tools/summary.js +176 -0
- package/dist/tools/update.d.ts +2 -0
- package/dist/tools/update.js +134 -0
- package/dist/worker.d.ts +15 -0
- package/dist/worker.js +700 -0
- package/package.json +59 -0
package/dist/ollama.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM chat completion — supports Ollama (local) and OpenAI (cloud)
|
|
3
|
+
* Set OPENAI_API_KEY to use OpenAI, otherwise falls back to Ollama
|
|
4
|
+
*/
|
|
5
|
+
function getOllamaUrl() {
|
|
6
|
+
return process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
7
|
+
}
|
|
8
|
+
function getOllamaChatModel() {
|
|
9
|
+
return process.env.OLLAMA_CHAT_MODEL ?? "qwen3:8b";
|
|
10
|
+
}
|
|
11
|
+
function getOpenAIKey() {
|
|
12
|
+
return process.env.OPENAI_API_KEY;
|
|
13
|
+
}
|
|
14
|
+
function getOpenAIChatModel() {
|
|
15
|
+
return process.env.OPENAI_CHAT_MODEL ?? "gpt-4o-mini";
|
|
16
|
+
}
|
|
17
|
+
async function ollamaChatImpl(messages) {
|
|
18
|
+
const model = getOllamaChatModel();
|
|
19
|
+
const response = await fetch(`${getOllamaUrl()}/api/chat`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
model,
|
|
24
|
+
messages,
|
|
25
|
+
stream: false,
|
|
26
|
+
format: "json",
|
|
27
|
+
options: { temperature: 0.2 },
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Ollama chat failed (${response.status}): ${await response.text()}. ` +
|
|
32
|
+
`請確認 Ollama 正在運行且已安裝 ${model} 模型。`);
|
|
33
|
+
}
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
return data.message.content;
|
|
36
|
+
}
|
|
37
|
+
async function openaiChatImpl(messages) {
|
|
38
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"Authorization": `Bearer ${getOpenAIKey()}`,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
model: getOpenAIChatModel(),
|
|
46
|
+
messages,
|
|
47
|
+
temperature: 0.2,
|
|
48
|
+
response_format: { type: "json_object" },
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`OpenAI chat failed (${response.status}): ${await response.text()}`);
|
|
53
|
+
}
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
return data.choices[0].message.content;
|
|
56
|
+
}
|
|
57
|
+
export async function ollamaChat(messages) {
|
|
58
|
+
if (getOpenAIKey()) {
|
|
59
|
+
return openaiChatImpl(messages);
|
|
60
|
+
}
|
|
61
|
+
return ollamaChatImpl(messages);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=ollama.js.map
|
package/dist/quota.d.ts
ADDED
package/dist/quota.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getSupabase } from "./supabase.js";
|
|
2
|
+
import { getUserLimits } from "./config.js";
|
|
3
|
+
export async function checkMonthlyQuota(userId) {
|
|
4
|
+
const supabase = getSupabase();
|
|
5
|
+
const limits = getUserLimits(userId);
|
|
6
|
+
const { data } = await supabase.rpc("get_monthly_trace_count", {
|
|
7
|
+
p_user_id: userId,
|
|
8
|
+
});
|
|
9
|
+
const used = typeof data === "number" ? data : 0;
|
|
10
|
+
return {
|
|
11
|
+
allowed: used < limits.traces,
|
|
12
|
+
used,
|
|
13
|
+
limit: limits.traces,
|
|
14
|
+
remaining: Math.max(0, limits.traces - used),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getUserLimits } from "./config.js";
|
|
2
|
+
const windows = new Map();
|
|
3
|
+
const WINDOW_MS = 60_000;
|
|
4
|
+
const CLEANUP_INTERVAL = 5 * 60_000;
|
|
5
|
+
let lastCleanup = Date.now();
|
|
6
|
+
function getWindow(userId, now) {
|
|
7
|
+
const ts = (windows.get(userId) || []).filter(t => now - t < WINDOW_MS);
|
|
8
|
+
if (ts.length === 0) {
|
|
9
|
+
windows.delete(userId);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
windows.set(userId, ts);
|
|
13
|
+
}
|
|
14
|
+
return ts;
|
|
15
|
+
}
|
|
16
|
+
function maybeCleanup(now) {
|
|
17
|
+
if (now - lastCleanup < CLEANUP_INTERVAL)
|
|
18
|
+
return;
|
|
19
|
+
lastCleanup = now;
|
|
20
|
+
for (const [userId, ts] of windows) {
|
|
21
|
+
if (ts.every(t => now - t >= WINDOW_MS)) {
|
|
22
|
+
windows.delete(userId);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function isRateLimited(userId) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
maybeCleanup(now);
|
|
29
|
+
const ts = getWindow(userId, now);
|
|
30
|
+
const maxReq = getUserLimits(userId).rateLimit;
|
|
31
|
+
return { limited: ts.length >= maxReq, count: ts.length };
|
|
32
|
+
}
|
|
33
|
+
export function recordRequest(userId) {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const ts = getWindow(userId, now);
|
|
36
|
+
ts.push(now);
|
|
37
|
+
windows.set(userId, ts);
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped user context
|
|
3
|
+
* CF Workers are single-threaded per request, so a simple global is safe
|
|
4
|
+
*/
|
|
5
|
+
let _currentUserId = null;
|
|
6
|
+
export function setCurrentUserId(id) {
|
|
7
|
+
_currentUserId = id;
|
|
8
|
+
}
|
|
9
|
+
export function getCurrentUserId() {
|
|
10
|
+
return _currentUserId;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=request-context.js.map
|
package/dist/supabase.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
let client = null;
|
|
3
|
+
export function getSupabase() {
|
|
4
|
+
if (client)
|
|
5
|
+
return client;
|
|
6
|
+
const url = process.env.SUPABASE_URL;
|
|
7
|
+
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
8
|
+
if (!url || !key) {
|
|
9
|
+
throw new Error("Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY environment variables. " +
|
|
10
|
+
"請設定 Supabase 連線資訊。");
|
|
11
|
+
}
|
|
12
|
+
client = createClient(url, key, {
|
|
13
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
14
|
+
});
|
|
15
|
+
return client;
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team access helper — resolves which user IDs a user can see traces from.
|
|
3
|
+
* Returns the user's own ID + all team members' IDs (for team knowledge access).
|
|
4
|
+
*/
|
|
5
|
+
import { getSupabase } from "./supabase.js";
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
const CACHE_TTL = 60_000; // 1 minute
|
|
8
|
+
export async function getVisibleAuthors(userId) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
const cached = cache.get(userId);
|
|
11
|
+
if (cached && now - cached.ts < CACHE_TTL)
|
|
12
|
+
return cached.ids;
|
|
13
|
+
const ids = new Set([userId]);
|
|
14
|
+
const supabase = getSupabase();
|
|
15
|
+
// Find all teams user is in
|
|
16
|
+
const { data: myTeams } = await supabase
|
|
17
|
+
.from("team_members")
|
|
18
|
+
.select("team_id")
|
|
19
|
+
.eq("user_id", userId);
|
|
20
|
+
if (myTeams && myTeams.length > 0) {
|
|
21
|
+
const teamIds = myTeams.map((t) => t.team_id);
|
|
22
|
+
// Get all members of those teams
|
|
23
|
+
const { data: members } = await supabase
|
|
24
|
+
.from("team_members")
|
|
25
|
+
.select("user_id")
|
|
26
|
+
.in("team_id", teamIds);
|
|
27
|
+
if (members) {
|
|
28
|
+
for (const m of members) {
|
|
29
|
+
ids.add(m.user_id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
cache.set(userId, { ids, ts: now });
|
|
34
|
+
return ids;
|
|
35
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getSupabase } from "../supabase.js";
|
|
3
|
+
import { getVisibleAuthors } from "../team-access.js";
|
|
4
|
+
export function registerActive(server, userId) {
|
|
5
|
+
server.tool("trapic_get_active", "Get all active traces, optionally filtered by tags. Returns the current organizational knowledge base. " +
|
|
6
|
+
"取得所有 active 狀態的 trace,可按標籤過濾。返回目前的組織知識庫。", {
|
|
7
|
+
tags: z.array(z.string()).optional().describe("Filter by tags (returns traces matching ANY of these tags). Leave empty for all active traces. " +
|
|
8
|
+
"按標籤過濾(符合任一標籤即返回)。留空則返回所有 active traces"),
|
|
9
|
+
limit: z.number().int().min(1).max(200).default(50).describe("Maximum number of results (1-200, default: 50). " +
|
|
10
|
+
"最多返回筆數(1-200,預設 50)"),
|
|
11
|
+
}, async (params) => {
|
|
12
|
+
try {
|
|
13
|
+
const supabase = getSupabase();
|
|
14
|
+
const { data, error } = await supabase.rpc("get_active_traces", {
|
|
15
|
+
filter_tags: params.tags ?? [],
|
|
16
|
+
result_limit: params.limit,
|
|
17
|
+
});
|
|
18
|
+
if (error) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: `Error getting active traces: ${error.message}\n取得 active traces 失敗:${error.message}`,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Post-filter: show own traces + team members' traces
|
|
29
|
+
const visibleAuthors = userId ? await getVisibleAuthors(userId) : null;
|
|
30
|
+
const filtered = visibleAuthors
|
|
31
|
+
? (data ?? []).filter((row) => visibleAuthors.has(row.author))
|
|
32
|
+
: (data ?? []);
|
|
33
|
+
const traces = filtered.map((row) => ({
|
|
34
|
+
id: row.id,
|
|
35
|
+
claim: row.claim,
|
|
36
|
+
reason: row.reason,
|
|
37
|
+
tags: row.tags,
|
|
38
|
+
caused_by: row.caused_by,
|
|
39
|
+
confidence: row.confidence,
|
|
40
|
+
author: row.author,
|
|
41
|
+
created_at: row.created_at,
|
|
42
|
+
}));
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
total: traces.length,
|
|
49
|
+
filter_tags: params.tags ?? [],
|
|
50
|
+
traces,
|
|
51
|
+
}, null, 2),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getSupabase } from "../supabase.js";
|
|
3
|
+
export function registerAssert(server) {
|
|
4
|
+
// trace.assert — 對一個 trace 表態
|
|
5
|
+
server.tool("trace.assert", "Assert a position on an existing trace — agree, disagree, amend, flag as outdated, or confirm. " +
|
|
6
|
+
"Different people can assert different positions on the same trace, building organizational consensus. " +
|
|
7
|
+
"對一個 trace 表態 — 同意、不同意、補充、標記過時或驗證確認。" +
|
|
8
|
+
"不同人可以對同一個 trace 表達不同立場,形成組織共識。", {
|
|
9
|
+
trace_id: z.string().uuid().describe("ID of the trace to assert on. " +
|
|
10
|
+
"要表態的 trace ID"),
|
|
11
|
+
author: z.string().uuid().describe("User ID of the person making the assertion. " +
|
|
12
|
+
"表態者的 user ID"),
|
|
13
|
+
type: z.enum(["agree", "disagree", "amend", "outdated", "confirm"]).describe("Type of assertion: " +
|
|
14
|
+
"agree (I support this), " +
|
|
15
|
+
"disagree (I have a different view), " +
|
|
16
|
+
"amend (I want to add a perspective), " +
|
|
17
|
+
"outdated (this is no longer valid), " +
|
|
18
|
+
"confirm (I verified this is correct). " +
|
|
19
|
+
"表態類型:agree 同意 / disagree 不同意 / amend 補充 / outdated 已過時 / confirm 驗證確認"),
|
|
20
|
+
comment: z.string().optional().describe("Why you hold this position. Required for disagree/amend/outdated, optional for agree/confirm. " +
|
|
21
|
+
"為什麼持此立場。disagree/amend/outdated 必須說明,agree/confirm 可省略。"),
|
|
22
|
+
linked_trace_id: z.string().uuid().optional().describe("For 'amend' type: link to a new trace that extends this one. " +
|
|
23
|
+
"amend 時可連結到補充用的新 trace"),
|
|
24
|
+
}, async (params) => {
|
|
25
|
+
try {
|
|
26
|
+
// Validate: disagree/amend/outdated should have a comment
|
|
27
|
+
if (["disagree", "amend", "outdated"].includes(params.type) &&
|
|
28
|
+
!params.comment?.trim()) {
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: `A comment is required for '${params.type}' assertions. Please explain your position.\n` +
|
|
34
|
+
`'${params.type}' 類型的表態需要提供理由。`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const supabase = getSupabase();
|
|
40
|
+
const record = {
|
|
41
|
+
trace_id: params.trace_id,
|
|
42
|
+
author: params.author,
|
|
43
|
+
type: params.type,
|
|
44
|
+
comment: params.comment ?? null,
|
|
45
|
+
linked_trace_id: params.linked_trace_id ?? null,
|
|
46
|
+
};
|
|
47
|
+
const { data, error } = await supabase
|
|
48
|
+
.from("trace_assertions")
|
|
49
|
+
.insert(record)
|
|
50
|
+
.select()
|
|
51
|
+
.single();
|
|
52
|
+
if (error) {
|
|
53
|
+
// Handle unique constraint violation
|
|
54
|
+
if (error.code === "23505") {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: `You already have a '${params.type}' assertion on this trace. Each person can only assert each type once per trace.\n` +
|
|
60
|
+
`你已經對此 trace 有 '${params.type}' 表態了。每人每種類型只能表態一次。`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: `Error creating assertion: ${error.message}\n建立表態失敗:${error.message}`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: JSON.stringify({
|
|
79
|
+
success: true,
|
|
80
|
+
message: "Assertion recorded / 表態已記錄",
|
|
81
|
+
assertion: {
|
|
82
|
+
id: data.id,
|
|
83
|
+
trace_id: data.trace_id,
|
|
84
|
+
type: data.type,
|
|
85
|
+
comment: data.comment,
|
|
86
|
+
linked_trace_id: data.linked_trace_id,
|
|
87
|
+
created_at: data.created_at,
|
|
88
|
+
},
|
|
89
|
+
}, null, 2),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// trace.consensus — 取得某個 trace 的共識摘要
|
|
102
|
+
server.tool("trace.consensus", "Get the consensus summary for a trace — how many people agree, disagree, amend, etc. " +
|
|
103
|
+
"Shows the collective organizational position on a knowledge proposition. " +
|
|
104
|
+
"取得某個 trace 的共識摘要 — 多少人同意、不同意、補充等。" +
|
|
105
|
+
"呈現組織對一個知識命題的集體立場。", {
|
|
106
|
+
trace_id: z.string().uuid().describe("ID of the trace to get consensus for. " +
|
|
107
|
+
"要查看共識的 trace ID"),
|
|
108
|
+
}, async (params) => {
|
|
109
|
+
try {
|
|
110
|
+
const supabase = getSupabase();
|
|
111
|
+
const { data, error } = await supabase.rpc("get_trace_consensus", {
|
|
112
|
+
target_trace_id: params.trace_id,
|
|
113
|
+
});
|
|
114
|
+
if (error) {
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: `Error getting consensus: ${error.message}\n取得共識摘要失敗:${error.message}`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: JSON.stringify(data, null, 2),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=assert.js.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getSupabase } from "../supabase.js";
|
|
3
|
+
export function registerChain(server, userId) {
|
|
4
|
+
// trace.get_chain — traverse upstream causal chain
|
|
5
|
+
server.tool("trapic_get_chain", "Get the causal chain of a trace — recursively follows caused_by links upstream to find root causes. " +
|
|
6
|
+
"取得 trace 的因果鏈 — 遞迴追溯 caused_by 上游,找到根本原因。", {
|
|
7
|
+
trace_id: z.string().uuid().describe("ID of the trace to start from. " +
|
|
8
|
+
"起始 trace 的 ID"),
|
|
9
|
+
max_depth: z.number().int().min(1).max(20).default(5).describe("Maximum recursion depth (1-20, default: 5). " +
|
|
10
|
+
"最大遞迴深度(1-20,預設 5)"),
|
|
11
|
+
}, async (params) => {
|
|
12
|
+
try {
|
|
13
|
+
const supabase = getSupabase();
|
|
14
|
+
const { data, error } = await supabase.rpc("get_trace_chain", {
|
|
15
|
+
trace_id: params.trace_id,
|
|
16
|
+
max_depth: params.max_depth,
|
|
17
|
+
});
|
|
18
|
+
if (error) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: `Error getting trace chain: ${error.message}\n取得因果鏈失敗:${error.message}`,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Post-filter by author for user isolation
|
|
29
|
+
const filtered = userId
|
|
30
|
+
? (data ?? []).filter((row) => row.author === userId)
|
|
31
|
+
: (data ?? []);
|
|
32
|
+
const chain = filtered.map((row) => ({
|
|
33
|
+
id: row.id,
|
|
34
|
+
claim: row.claim,
|
|
35
|
+
reason: row.reason,
|
|
36
|
+
status: row.status,
|
|
37
|
+
tags: row.tags,
|
|
38
|
+
caused_by: row.caused_by,
|
|
39
|
+
depth: row.depth,
|
|
40
|
+
}));
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: JSON.stringify({
|
|
46
|
+
trace_id: params.trace_id,
|
|
47
|
+
chain_length: chain.length,
|
|
48
|
+
chain,
|
|
49
|
+
}, null, 2),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// trace.get_effects — traverse downstream effects
|
|
62
|
+
server.tool("trapic_get_effects", "Get downstream effects of a trace — finds all traces whose caused_by includes this trace, recursively. " +
|
|
63
|
+
"取得 trace 的下游影響 — 遞迴查找所有 caused_by 包含此 trace 的後續 trace。", {
|
|
64
|
+
trace_id: z.string().uuid().describe("ID of the trace to find effects for. " +
|
|
65
|
+
"要查找影響的 trace ID"),
|
|
66
|
+
max_depth: z.number().int().min(1).max(20).default(5).describe("Maximum recursion depth (1-20, default: 5). " +
|
|
67
|
+
"最大遞迴深度(1-20,預設 5)"),
|
|
68
|
+
}, async (params) => {
|
|
69
|
+
try {
|
|
70
|
+
const supabase = getSupabase();
|
|
71
|
+
const { data, error } = await supabase.rpc("get_trace_effects", {
|
|
72
|
+
trace_id: params.trace_id,
|
|
73
|
+
max_depth: params.max_depth,
|
|
74
|
+
});
|
|
75
|
+
if (error) {
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: `Error getting trace effects: ${error.message}\n取得下游影響失敗:${error.message}`,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Post-filter by author for user isolation
|
|
86
|
+
const filteredEffects = userId
|
|
87
|
+
? (data ?? []).filter((row) => row.author === userId)
|
|
88
|
+
: (data ?? []);
|
|
89
|
+
const effects = filteredEffects.map((row) => ({
|
|
90
|
+
id: row.id,
|
|
91
|
+
claim: row.claim,
|
|
92
|
+
reason: row.reason,
|
|
93
|
+
status: row.status,
|
|
94
|
+
tags: row.tags,
|
|
95
|
+
caused_by: row.caused_by,
|
|
96
|
+
depth: row.depth,
|
|
97
|
+
}));
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: JSON.stringify({
|
|
103
|
+
trace_id: params.trace_id,
|
|
104
|
+
effects_count: effects.length,
|
|
105
|
+
effects,
|
|
106
|
+
}, null, 2),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export declare function contextualizeTrace(traceId: string): Promise<{
|
|
3
|
+
success: boolean;
|
|
4
|
+
actions: string[];
|
|
5
|
+
error?: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function registerContext(server: McpServer, userId: string | null): void;
|