codetrap 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/.env.example +3 -0
- package/LICENSE +22 -0
- package/README.md +305 -0
- package/docs/installation.md +306 -0
- package/package.json +62 -0
- package/scripts/build-release.ts +64 -0
- package/scripts/check-release-version.ts +19 -0
- package/skills/codetrap-add/SKILL.md +65 -0
- package/skills/codetrap-check/SKILL.md +47 -0
- package/skills/codetrap-search/SKILL.md +43 -0
- package/src/commands/router.ts +407 -0
- package/src/db/connection.ts +36 -0
- package/src/db/embedding-queries.ts +154 -0
- package/src/db/queries.ts +296 -0
- package/src/db/repository.ts +141 -0
- package/src/db/schema.ts +205 -0
- package/src/domain/trap.ts +304 -0
- package/src/index.ts +58 -0
- package/src/lib/constants.ts +56 -0
- package/src/lib/embedder.ts +133 -0
- package/src/lib/embedding-job.ts +68 -0
- package/src/lib/format.ts +97 -0
- package/src/lib/fts-query.ts +17 -0
- package/src/lib/scope.ts +30 -0
- package/src/lib/search-normalizer.ts +92 -0
- package/src/lib/search-result-card.ts +38 -0
- package/src/lib/search-service.ts +189 -0
- package/src/lib/store.ts +272 -0
- package/src/lib/trap-archive.ts +91 -0
- package/src/lib/trap-json-fields.ts +42 -0
- package/src/lib/trap-operations.ts +127 -0
- package/src/lib/trap-search-document.ts +73 -0
- package/src/mcp/resources.ts +26 -0
- package/src/mcp/server.ts +167 -0
- package/src/mcp/tools.ts +106 -0
- package/src/mcp-server.ts +6 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
ListResourcesRequestSchema,
|
|
7
|
+
ReadResourceRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { TrapStore } from "../lib/store";
|
|
10
|
+
import { toolDefinitions } from "./tools";
|
|
11
|
+
import { resourceDefinitions } from "./resources";
|
|
12
|
+
import { TrapOperations } from "../lib/trap-operations";
|
|
13
|
+
|
|
14
|
+
type ToolArgs = Record<string, any>;
|
|
15
|
+
|
|
16
|
+
export async function handleToolCall(store: TrapStore, name: string, args: ToolArgs) {
|
|
17
|
+
const operations = new TrapOperations(store);
|
|
18
|
+
try {
|
|
19
|
+
switch (name) {
|
|
20
|
+
case "search_traps": {
|
|
21
|
+
const cards = await operations.searchTrapCards({
|
|
22
|
+
query: args.query,
|
|
23
|
+
scope: args.scope,
|
|
24
|
+
category: args.category,
|
|
25
|
+
mode: args.mode,
|
|
26
|
+
limit: args.limit ?? 20,
|
|
27
|
+
status: args.status,
|
|
28
|
+
});
|
|
29
|
+
return { content: [{ type: "text", text: JSON.stringify(cards, null, 2) }] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
case "add_trap": {
|
|
33
|
+
const result = operations.addTrap(args);
|
|
34
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case "get_trap": {
|
|
38
|
+
const result = operations.getTrapDetails(args.id, args.scope);
|
|
39
|
+
if (!result) {
|
|
40
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "not found" }) }], isError: true };
|
|
41
|
+
}
|
|
42
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case "list_traps": {
|
|
46
|
+
const groups = operations.listTraps({
|
|
47
|
+
scope: args.scope,
|
|
48
|
+
category: args.category,
|
|
49
|
+
status: args.status,
|
|
50
|
+
limit: args.limit ?? 50,
|
|
51
|
+
});
|
|
52
|
+
const flat = groups.flatMap((g) =>
|
|
53
|
+
g.traps.map((t) => ({ ...t, scope: g.scope }))
|
|
54
|
+
);
|
|
55
|
+
return { content: [{ type: "text", text: JSON.stringify(flat, null, 2) }] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case "update_trap": {
|
|
59
|
+
const result = operations.updateTrap(args.id, args, args.scope);
|
|
60
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "delete_trap": {
|
|
64
|
+
const result = operations.deleteTrap(args.id, args.scope);
|
|
65
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "add_trap_evidence": {
|
|
69
|
+
const result = operations.addTrapEvidence(args.id, args, args.scope);
|
|
70
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], isError: !result.success };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case "archive_trap": {
|
|
74
|
+
const result = operations.archiveTrap(args.id, args.scope);
|
|
75
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], isError: !result.success };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case "supersede_trap": {
|
|
79
|
+
const result = operations.supersedeTrap(args.id, args.superseded_by_id, args.scope, args.state_key);
|
|
80
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], isError: !result.success };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case "get_stats": {
|
|
84
|
+
const stats = operations.getStats();
|
|
85
|
+
const out: Record<string, unknown> = { global: stats.global };
|
|
86
|
+
if (stats.project) out.project = stats.project;
|
|
87
|
+
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
default:
|
|
91
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
92
|
+
}
|
|
93
|
+
} catch (e: any) {
|
|
94
|
+
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function start(): Promise<void> {
|
|
99
|
+
const store = new TrapStore(process.cwd());
|
|
100
|
+
|
|
101
|
+
const server = new Server(
|
|
102
|
+
{ name: "codetrap", version: "0.1.0" },
|
|
103
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// List tools
|
|
107
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
108
|
+
tools: toolDefinitions,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// Call tool
|
|
112
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
113
|
+
const { name } = request.params;
|
|
114
|
+
const args = (request.params.arguments ?? {}) as ToolArgs;
|
|
115
|
+
return handleToolCall(store, name, args);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// List resources
|
|
119
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
120
|
+
resources: resourceDefinitions,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
// Read resource
|
|
124
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
125
|
+
const uri = request.params.uri;
|
|
126
|
+
try {
|
|
127
|
+
switch (uri) {
|
|
128
|
+
case "codetrap://project/recent": {
|
|
129
|
+
const groups = store.list({ scope: "project", limit: 10 });
|
|
130
|
+
const traps = groups.flatMap((g) => g.traps);
|
|
131
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(traps, null, 2) }] };
|
|
132
|
+
}
|
|
133
|
+
case "codetrap://global/recent": {
|
|
134
|
+
const groups = store.list({ scope: "global", limit: 10 });
|
|
135
|
+
const traps = groups.flatMap((g) => g.traps);
|
|
136
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(traps, null, 2) }] };
|
|
137
|
+
}
|
|
138
|
+
case "codetrap://project/top": {
|
|
139
|
+
const traps = store.topTraps("project", 20);
|
|
140
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(traps, null, 2) }] };
|
|
141
|
+
}
|
|
142
|
+
case "codetrap://global/top": {
|
|
143
|
+
const traps = store.topTraps("global", 20);
|
|
144
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(traps, null, 2) }] };
|
|
145
|
+
}
|
|
146
|
+
default: {
|
|
147
|
+
// Handle codetrap://{scope}/trap/{id}
|
|
148
|
+
const match = uri.match(/^codetrap:\/\/(project|global)\/trap\/(\d+)$/);
|
|
149
|
+
if (match) {
|
|
150
|
+
const result = store.get(parseInt(match[2]), match[1]);
|
|
151
|
+
if (!result) {
|
|
152
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "not found" }) }] };
|
|
153
|
+
}
|
|
154
|
+
const details = store.getDetails(parseInt(match[2]), match[1]);
|
|
155
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(details ?? result.trap, null, 2) }] };
|
|
156
|
+
}
|
|
157
|
+
return { contents: [{ uri, mimeType: "text/plain", text: "Unknown resource" }] };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (e: any) {
|
|
161
|
+
return { contents: [{ uri, mimeType: "text/plain", text: e.message }] };
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const transport = new StdioServerTransport();
|
|
166
|
+
await server.connect(transport);
|
|
167
|
+
}
|
package/src/mcp/tools.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CATEGORIES, SCOPES, SEARCH_MODES, TRAP_STATUSES } from "../lib/constants";
|
|
2
|
+
import {
|
|
3
|
+
archiveTrapInputSchema,
|
|
4
|
+
supersedeTrapInputSchema,
|
|
5
|
+
trapEvidenceInputSchema,
|
|
6
|
+
trapInputSchema,
|
|
7
|
+
trapUpdateSchema,
|
|
8
|
+
} from "../domain/trap";
|
|
9
|
+
|
|
10
|
+
const categoryEnum = [...CATEGORIES] as string[];
|
|
11
|
+
const scopeEnum = [...SCOPES] as string[];
|
|
12
|
+
const searchModeEnum = [...SEARCH_MODES] as string[];
|
|
13
|
+
const statusEnum = [...TRAP_STATUSES, "all"] as string[];
|
|
14
|
+
|
|
15
|
+
export const toolDefinitions = [
|
|
16
|
+
{
|
|
17
|
+
name: "search_traps",
|
|
18
|
+
description:
|
|
19
|
+
"Search for relevant coding pitfalls. Returns compact action cards by default; call get_trap with the returned id and scope before making code changes when details or examples are needed.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
query: { type: "string", description: "Search query for full-text search" },
|
|
24
|
+
scope: { type: "string", enum: scopeEnum, description: "Limit to a specific scope" },
|
|
25
|
+
category: { type: "string", enum: categoryEnum, description: "Filter by category" },
|
|
26
|
+
mode: { type: "string", enum: searchModeEnum, description: "Search mode (default hybrid)" },
|
|
27
|
+
limit: { type: "number", description: "Max results (default 20)" },
|
|
28
|
+
status: { type: "string", enum: statusEnum, description: "Lifecycle filter (default active; use all for history)" },
|
|
29
|
+
},
|
|
30
|
+
required: ["query"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "add_trap",
|
|
35
|
+
description:
|
|
36
|
+
"Record a new coding pitfall. Call this when the user wants to save a lesson learned: an AI mistake pattern and the correct approach.",
|
|
37
|
+
inputSchema: trapInputSchema(),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "get_trap",
|
|
41
|
+
description: "Drill down into a trap returned by search_traps. Use the id and scope from the action card to get full details, examples, lifecycle, and evidence.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
id: { type: "number", description: "Trap ID" },
|
|
46
|
+
scope: { type: "string", enum: scopeEnum, description: "Which scope to look in" },
|
|
47
|
+
},
|
|
48
|
+
required: ["id"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "list_traps",
|
|
53
|
+
description: "List traps with optional filters.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
scope: { type: "string", enum: scopeEnum },
|
|
58
|
+
category: { type: "string", enum: categoryEnum },
|
|
59
|
+
status: { type: "string", enum: statusEnum, description: "Lifecycle filter (default active; use all for history)" },
|
|
60
|
+
limit: { type: "number", description: "Max results (default 50)" },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "update_trap",
|
|
66
|
+
description: "Update an existing trap's fields.",
|
|
67
|
+
inputSchema: trapUpdateSchema(),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "delete_trap",
|
|
71
|
+
description: "Delete a trap by ID.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
id: { type: "number", description: "Trap ID to delete" },
|
|
76
|
+
scope: { type: "string", enum: scopeEnum },
|
|
77
|
+
},
|
|
78
|
+
required: ["id"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "add_trap_evidence",
|
|
83
|
+
description: "Attach traceable evidence/source metadata to an existing trap.",
|
|
84
|
+
inputSchema: trapEvidenceInputSchema(),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "archive_trap",
|
|
88
|
+
description: "Archive an active trap so default search no longer returns it while history remains recoverable.",
|
|
89
|
+
inputSchema: archiveTrapInputSchema(),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "supersede_trap",
|
|
93
|
+
description: "Mark one trap as superseded by another trap in the same scope.",
|
|
94
|
+
inputSchema: supersedeTrapInputSchema(),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "get_stats",
|
|
98
|
+
description: "Get statistics about the trap database: total counts, breakdown by category and severity.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
scope: { type: "string", enum: scopeEnum },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
];
|