context-mcp-server 1.0.6 → 1.0.8
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/README.md +98 -359
- package/codegraph/server.py +37 -46
- package/package.json +4 -3
- package/pyproject.toml +1 -1
- package/src/assests/main.png +0 -0
- package/src/cli.js +69 -57
- package/src/db.js +946 -798
- package/src/guard.js +9 -3
- package/src/migrator.js +124 -0
- package/src/server.js +7 -6
- package/src/templates/AGENTS.md +6 -13
- package/src/templates/CLAUDE.md +6 -12
- package/src/templates/GEMINI.md +6 -13
- package/src/templates/commands/context-resume.md +1 -1
- package/src/templates/commands/save-context.md +1 -1
- package/src/templates/cursor-rules.mdc +3 -3
- package/src/templates/skills/SKILL.md +9 -16
- package/src/templates/windsurf-rules.md +3 -3
- package/src/tools/codegraph.js +8 -77
- package/src/tools/context.js +23 -11
- package/src/tools/gitTools.js +1 -3
- package/src/tools/plan.js +130 -0
- package/uv.lock +1 -1
- package/codegraph/extractors/audio_extractor.py +0 -8
- package/codegraph/extractors/doc_extractor.py +0 -34
- package/codegraph/extractors/image_extractor.py +0 -26
- package/src/tools/discussion.js +0 -123
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
saveDiscussion, getDiscussion, listDiscussions,
|
|
5
|
+
deleteDiscussion, updateDiscussion,
|
|
6
|
+
} from '../db.js';
|
|
7
|
+
|
|
8
|
+
function writePlanFile(planDir, name, content, title) {
|
|
9
|
+
if (!planDir) return null;
|
|
10
|
+
const dir = resolve(planDir);
|
|
11
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
12
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
13
|
+
const filePath = join(dir, `${slug}.md`);
|
|
14
|
+
const md = `# ${title || name}\n\n${content || ''}\n`;
|
|
15
|
+
writeFileSync(filePath, md, 'utf8');
|
|
16
|
+
return filePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const definition = {
|
|
20
|
+
name: 'plan',
|
|
21
|
+
description:
|
|
22
|
+
`AI plan storage — auto-call this tool whenever you create any kind of plan.\n` +
|
|
23
|
+
`Saves a summary of the plan to the project store and writes a .md file to planDir.\n` +
|
|
24
|
+
`• "save" — Store a new plan or overwrite an existing one by name. Pass planDir to write a file.\n` +
|
|
25
|
+
`• "update" — Patch title/content/status on an existing plan.\n` +
|
|
26
|
+
`• "get" — Retrieve a plan by name or id (full content).\n` +
|
|
27
|
+
`• "list" — List all plans for the project.\n` +
|
|
28
|
+
`• "delete" — Remove a plan by name or id.`,
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
action: { type: 'string', enum: ['save', 'get', 'list', 'update', 'delete'] },
|
|
33
|
+
name: { type: 'string', description: 'Short slug-style identifier for the plan, e.g. "auth-refactor"' },
|
|
34
|
+
id: { type: 'string' },
|
|
35
|
+
project: { type: 'string' },
|
|
36
|
+
title: { type: 'string', description: 'Human-readable plan title' },
|
|
37
|
+
content: { type: 'string', description: 'Full plan summary in markdown' },
|
|
38
|
+
status: { type: 'string', enum: ['active', 'done'] },
|
|
39
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
40
|
+
planDir: { type: 'string', description: 'Absolute path to the folder where .md plan files are written. Pass the path for your AI platform (e.g. ~/.claude/plans/ for Claude Code).' },
|
|
41
|
+
},
|
|
42
|
+
required: ['action'],
|
|
43
|
+
},
|
|
44
|
+
outputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
success: { type: 'boolean' },
|
|
48
|
+
id: { type: 'string' },
|
|
49
|
+
name: { type: 'string' },
|
|
50
|
+
filePath: { type: 'string' },
|
|
51
|
+
plan: { type: 'object' },
|
|
52
|
+
plans: { type: 'array' },
|
|
53
|
+
message: { type: 'string' },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export async function handle(args, state) {
|
|
59
|
+
if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
|
|
60
|
+
|
|
61
|
+
switch (args.action) {
|
|
62
|
+
case 'save': {
|
|
63
|
+
if (!args.name) throw new Error('name is required for save');
|
|
64
|
+
if (!args.content) throw new Error('content is required for save');
|
|
65
|
+
const plan = saveDiscussion({
|
|
66
|
+
name: args.name,
|
|
67
|
+
title: args.title || args.name,
|
|
68
|
+
content: args.content,
|
|
69
|
+
project: args.project,
|
|
70
|
+
tags: args.tags,
|
|
71
|
+
type: 'plan',
|
|
72
|
+
status: args.status || 'active',
|
|
73
|
+
sessionId: state.sessionId || null,
|
|
74
|
+
});
|
|
75
|
+
if (plan.status === 'active') state.discussionId = plan.id;
|
|
76
|
+
const filePath = writePlanFile(args.planDir, args.name, args.content, args.title);
|
|
77
|
+
return {
|
|
78
|
+
success: true, id: plan.id, name: plan.name,
|
|
79
|
+
filePath: filePath || undefined,
|
|
80
|
+
message: `Plan "${plan.name}" saved.${filePath ? ` Written to ${filePath}` : ''}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case 'update': {
|
|
85
|
+
if (!args.name && !args.id) throw new Error('name or id is required for update');
|
|
86
|
+
const updated = updateDiscussion({
|
|
87
|
+
name: args.name,
|
|
88
|
+
id: args.id,
|
|
89
|
+
title: args.title,
|
|
90
|
+
content: args.content,
|
|
91
|
+
status: args.status,
|
|
92
|
+
tags: args.tags,
|
|
93
|
+
});
|
|
94
|
+
if (!updated) throw new Error(`No plan found for "${args.name || args.id}".`);
|
|
95
|
+
if (updated.status !== 'active' && state.discussionId === updated.id) state.discussionId = null;
|
|
96
|
+
if (updated.status === 'active') state.discussionId = updated.id;
|
|
97
|
+
const filePath = writePlanFile(args.planDir, updated.name, updated.content, updated.title);
|
|
98
|
+
return {
|
|
99
|
+
success: true, id: updated.id, name: updated.name, status: updated.status,
|
|
100
|
+
filePath: filePath || undefined,
|
|
101
|
+
message: `Plan "${updated.name}" updated (${updated.status}).`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'get': {
|
|
106
|
+
if (!args.name && !args.id) throw new Error('name or id is required for get');
|
|
107
|
+
const plan = getDiscussion({ name: args.name, id: args.id, project: args.project });
|
|
108
|
+
return plan
|
|
109
|
+
? { plan }
|
|
110
|
+
: { plan: null, message: `No plan found for "${args.name || args.id}".` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'list': {
|
|
114
|
+
const plans = listDiscussions({ project: args.project, status: args.status });
|
|
115
|
+
return { plans };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'delete': {
|
|
119
|
+
if (!args.name && !args.id) throw new Error('name or id is required for delete');
|
|
120
|
+
const result = deleteDiscussion({ name: args.name, id: args.id });
|
|
121
|
+
if (state.discussionId && (args.id === state.discussionId || result.deleted > 0)) {
|
|
122
|
+
state.discussionId = null;
|
|
123
|
+
}
|
|
124
|
+
return { ...result, message: `Deleted ${result.deleted} plan(s).` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
default:
|
|
128
|
+
throw new Error(`Unknown plan action: ${args.action}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
package/uv.lock
CHANGED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
doc_extractor.py — extract plain text from doc and PDF files.
|
|
3
|
-
PDF extraction uses pymupdf if installed; falls back to label-only otherwise.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def extract_text(path: str) -> str:
|
|
10
|
-
"""Return text content of a doc/PDF file. Truncated at DOC_MAX_CHARS."""
|
|
11
|
-
from ..config import DOC_MAX_CHARS
|
|
12
|
-
if path.lower().endswith(".pdf"):
|
|
13
|
-
return _extract_pdf(path, DOC_MAX_CHARS)
|
|
14
|
-
try:
|
|
15
|
-
return Path(path).read_text(encoding="utf-8", errors="replace")[:DOC_MAX_CHARS]
|
|
16
|
-
except OSError:
|
|
17
|
-
return ""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _extract_pdf(path: str, max_chars: int) -> str:
|
|
21
|
-
try:
|
|
22
|
-
import pymupdf # optional dep
|
|
23
|
-
doc = pymupdf.open(path)
|
|
24
|
-
parts = []
|
|
25
|
-
for page in doc:
|
|
26
|
-
parts.append(page.get_text())
|
|
27
|
-
if sum(len(p) for p in parts) >= max_chars:
|
|
28
|
-
break
|
|
29
|
-
doc.close()
|
|
30
|
-
return "".join(parts)[:max_chars]
|
|
31
|
-
except ImportError:
|
|
32
|
-
return f"[PDF: {Path(path).name} — install pymupdf to extract text]"
|
|
33
|
-
except Exception:
|
|
34
|
-
return ""
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
image_extractor.py — encode images as base64 for AI vision.
|
|
3
|
-
No external deps — stdlib only.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import base64
|
|
7
|
-
import mimetypes
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def extract_image_b64(path: str) -> dict | None:
|
|
12
|
-
"""Return {"data": base64_str, "media_type": "image/png"} or None on failure."""
|
|
13
|
-
try:
|
|
14
|
-
data = Path(path).read_bytes()
|
|
15
|
-
media_type = mimetypes.guess_type(path)[0] or "image/png"
|
|
16
|
-
return {"data": base64.b64encode(data).decode(), "media_type": media_type}
|
|
17
|
-
except OSError:
|
|
18
|
-
return None
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def extract_svg_text(path: str) -> str:
|
|
22
|
-
"""Return SVG file as plain text (SVGs are XML — readable as-is)."""
|
|
23
|
-
try:
|
|
24
|
-
return Path(path).read_text(encoding="utf-8", errors="replace")[:4000]
|
|
25
|
-
except OSError:
|
|
26
|
-
return ""
|
package/src/tools/discussion.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
saveDiscussion, getDiscussion, listDiscussions, deleteDiscussion,
|
|
3
|
-
updateDiscussion, updateDiscussionStep, clearDiscussionLink,
|
|
4
|
-
} from '../db.js';
|
|
5
|
-
|
|
6
|
-
export const definition = {
|
|
7
|
-
name: 'discussion',
|
|
8
|
-
description:
|
|
9
|
-
`Forward-looking thinking space — plans, research, ideas, design.\n` +
|
|
10
|
-
`• "save" — Create or update a discussion.\n` +
|
|
11
|
-
`• "update" — Patch specific fields without touching the rest.\n` +
|
|
12
|
-
`• "get" — Retrieve by name or id (full content + steps).\n` +
|
|
13
|
-
`• "list" — List discussions (filterable). Header + stepsSummary only.\n` +
|
|
14
|
-
`• "delete" — Remove by name or id.\n` +
|
|
15
|
-
`• "update_step" — Mark a step done/in-progress. Auto-closes when all done.`,
|
|
16
|
-
inputSchema: {
|
|
17
|
-
type: 'object',
|
|
18
|
-
properties: {
|
|
19
|
-
action: { type: 'string', enum: ['save', 'get', 'list', 'delete', 'update', 'update_step'] },
|
|
20
|
-
name: { type: 'string' },
|
|
21
|
-
id: { type: 'string' },
|
|
22
|
-
project: { type: 'string' },
|
|
23
|
-
title: { type: 'string' },
|
|
24
|
-
description: { type: 'string' },
|
|
25
|
-
content: { type: 'string' },
|
|
26
|
-
type: { type: 'string', enum: ['plan', 'research', 'idea', 'design', 'implementation', 'review', 'thread'] },
|
|
27
|
-
status: { type: 'string', enum: ['active', 'done'] },
|
|
28
|
-
tags: { type: 'array', items: { type: 'string' } },
|
|
29
|
-
parentId: { type: 'string' },
|
|
30
|
-
linkedContextIds: { type: 'array', items: { type: 'string' } },
|
|
31
|
-
steps: { type: 'array', items: { type: 'object' } },
|
|
32
|
-
stepId: { type: 'string' },
|
|
33
|
-
stepStatus: { type: 'string', enum: ['pending', 'in-progress', 'done', 'skipped'] },
|
|
34
|
-
linkedContextId: { type: 'string' },
|
|
35
|
-
filterStatus: { type: 'string' },
|
|
36
|
-
filterType: { type: 'string' },
|
|
37
|
-
},
|
|
38
|
-
required: ['action'],
|
|
39
|
-
},
|
|
40
|
-
outputSchema: {
|
|
41
|
-
type: 'object',
|
|
42
|
-
properties: {
|
|
43
|
-
success: { type: 'boolean' },
|
|
44
|
-
id: { type: 'string' },
|
|
45
|
-
name: { type: 'string' },
|
|
46
|
-
discussion: { type: 'object' },
|
|
47
|
-
discussions: { type: 'array' },
|
|
48
|
-
step: { type: 'object' },
|
|
49
|
-
message: { type: 'string' },
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export async function handle(args, state) {
|
|
55
|
-
switch (args.action) {
|
|
56
|
-
case 'save': {
|
|
57
|
-
if (!args.name) throw new Error('name is required for save');
|
|
58
|
-
const disc = saveDiscussion({ ...args, sessionId: args.sessionId || state.sessionId });
|
|
59
|
-
if (disc.status === 'active') state.discussionId = disc.id;
|
|
60
|
-
else if (state.discussionId === disc.id) state.discussionId = null;
|
|
61
|
-
return { success: true, id: disc.id, name: disc.name,
|
|
62
|
-
message: `Discussion "${disc.name}" saved (${disc.type}, ${disc.status}).` };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
case 'get': {
|
|
66
|
-
if (!args.name && !args.id) throw new Error('name or id is required for get');
|
|
67
|
-
const disc = getDiscussion({ name: args.name, id: args.id, project: args.project });
|
|
68
|
-
return disc
|
|
69
|
-
? { discussion: disc }
|
|
70
|
-
: { discussion: null, message: `No discussion found for "${args.name || args.id}".` };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
case 'update': {
|
|
74
|
-
if (!args.name && !args.id) throw new Error('name or id is required for update');
|
|
75
|
-
const updated = updateDiscussion({ ...args });
|
|
76
|
-
if (!updated) throw new Error(`No discussion found for "${args.name || args.id}".`);
|
|
77
|
-
if (updated.status !== 'active' && state.discussionId === updated.id) state.discussionId = null;
|
|
78
|
-
if (updated.status === 'active') state.discussionId = updated.id;
|
|
79
|
-
return { success: true, id: updated.id, name: updated.name, status: updated.status,
|
|
80
|
-
message: `Discussion "${updated.name}" updated (${updated.status}).` };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
case 'list': {
|
|
84
|
-
return { discussions: listDiscussions({ project: args.project, status: args.filterStatus, type: args.filterType }) };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
case 'delete': {
|
|
88
|
-
if (!args.name && !args.id) throw new Error('name or id is required for delete');
|
|
89
|
-
const toDelete = getDiscussion({ name: args.name, id: args.id });
|
|
90
|
-
if (toDelete) {
|
|
91
|
-
for (const ctxId of (toDelete.linkedContextIds || [])) clearDiscussionLink(ctxId);
|
|
92
|
-
}
|
|
93
|
-
const del = deleteDiscussion({ name: args.name, id: args.id });
|
|
94
|
-
if (state.discussionId) {
|
|
95
|
-
const matchById = args.id && args.id === state.discussionId;
|
|
96
|
-
const matchByName = args.name && del.deleted > 0;
|
|
97
|
-
if (matchById || matchByName) state.discussionId = null;
|
|
98
|
-
}
|
|
99
|
-
return { ...del, message: `Deleted ${del.deleted} discussion(s).` };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
case 'update_step': {
|
|
103
|
-
if (!args.name && !args.id) throw new Error('name or id is required for update_step');
|
|
104
|
-
if (!args.stepId) throw new Error('stepId is required for update_step');
|
|
105
|
-
const updated = updateDiscussionStep({
|
|
106
|
-
discussionName: args.name,
|
|
107
|
-
discussionId: args.id,
|
|
108
|
-
stepId: args.stepId,
|
|
109
|
-
status: args.stepStatus,
|
|
110
|
-
linkedContextId: args.linkedContextId,
|
|
111
|
-
});
|
|
112
|
-
if (!updated) throw new Error('Discussion or step not found.');
|
|
113
|
-
if (updated.discussion.status === 'done' && state.discussionId === updated.discussion.id) {
|
|
114
|
-
state.discussionId = null;
|
|
115
|
-
}
|
|
116
|
-
return { success: true, step: updated.step, discussionStatus: updated.discussion.status,
|
|
117
|
-
message: `Step updated. Discussion "${updated.discussion.name}" is now "${updated.discussion.status}".` };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
default:
|
|
121
|
-
throw new Error(`Unknown discussion action: ${args.action}`);
|
|
122
|
-
}
|
|
123
|
-
}
|