@chamba/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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chamba contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @chamba/mcp
2
+
3
+ The **chamba MCP server** — adds orchestration, workspace context, git worktrees and
4
+ Obsidian memory to any MCP-capable editor (Cursor, Claude Code, VS Code/Copilot,
5
+ Windsurf, Cline, OpenCode).
6
+
7
+ **No API key.** chamba never calls an LLM — your editor's model does the reasoning
8
+ and calls these tools.
9
+
10
+ ## Install (in your editor)
11
+
12
+ **Cursor** — `.cursor/mcp.json`:
13
+
14
+ ```json
15
+ { "mcpServers": { "chamba": { "command": "npx", "args": ["-y", "@chamba/mcp"] } } }
16
+ ```
17
+
18
+ **Claude Code**: `claude mcp add chamba -- npx -y @chamba/mcp`
19
+
20
+ **VS Code** — `.vscode/mcp.json` (note: VS Code uses `"servers"`, not `"mcpServers"`):
21
+
22
+ ```json
23
+ { "servers": { "chamba": { "type": "stdio", "command": "npx", "args": ["-y", "@chamba/mcp"] } } }
24
+ ```
25
+
26
+ Point at an Obsidian vault with `"env": { "CHAMBA_OBSIDIAN_VAULT_PATH": "/path/to/vault" }`.
27
+
28
+ ## Tools
29
+
30
+ `chamba_workspace_init` · `chamba_workspace_show` · `chamba_workspace_reload` ·
31
+ `chamba_load_context` · `chamba_summarize_to_vault` · `chamba_generate_plan` ·
32
+ `chamba_review_plan` · `chamba_create_worktree` · `chamba_list_worktrees` ·
33
+ `chamba_cleanup_worktree` · `chamba_remember` · `chamba_recall`
34
+
35
+ Full docs, per-editor guides and examples:
36
+ **https://github.com/thelord07/chamba**
37
+
38
+ ## License
39
+
40
+ MIT
package/bin/chamba-mcp ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/main.js';
package/dist/main.js ADDED
@@ -0,0 +1,564 @@
1
+ // src/main.ts
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+
4
+ // src/logging.ts
5
+ import { mkdirSync } from "fs";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ import { destination, pino } from "pino";
9
+ function createLogger() {
10
+ const logDir = join(homedir(), ".chamba", "logs");
11
+ mkdirSync(logDir, { recursive: true });
12
+ const logPath = join(logDir, `mcp-${process.pid}.log`);
13
+ return pino(
14
+ { level: process.env.CHAMBA_LOG_LEVEL ?? "info" },
15
+ destination({ dest: logPath, sync: false })
16
+ );
17
+ }
18
+
19
+ // src/server.ts
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+
22
+ // src/services.ts
23
+ import { homedir as homedir2 } from "os";
24
+ import { NodeFilesystem, NodeProcess, SystemClock } from "@chamba/adapters";
25
+ function createNodeServices() {
26
+ return {
27
+ fs: new NodeFilesystem(),
28
+ process: new NodeProcess(),
29
+ clock: new SystemClock(),
30
+ cwd: process.cwd(),
31
+ homedir: homedir2(),
32
+ obsidianVaultPath: process.env.CHAMBA_OBSIDIAN_VAULT_PATH
33
+ };
34
+ }
35
+ function obsidianSearchRoots(services) {
36
+ const { homedir: home, cwd } = services;
37
+ return [cwd, `${home}/Documents`, `${home}/Notes`, `${home}/Obsidian`, `${home}/obsidian`, home];
38
+ }
39
+
40
+ // src/tools/cleanup-worktree.ts
41
+ import { WorktreeError, WorktreeManager } from "@chamba/core";
42
+ import { z } from "zod";
43
+ var TOOL_NAME = "chamba_cleanup_worktree";
44
+ var DESCRIPTION = "Remove a worktree directory while KEEPING its branch. Never deletes the branch and never merges \u2014 the branch stays for you to review and merge by hand. Runs `git worktree remove` without --force, so a dirty worktree fails loudly.";
45
+ function registerCleanupWorktree(server, logger, services) {
46
+ server.registerTool(
47
+ TOOL_NAME,
48
+ {
49
+ title: "Cleanup worktree",
50
+ description: DESCRIPTION,
51
+ inputSchema: {
52
+ branch: z.string().describe("Branch whose worktree directory should be removed.")
53
+ }
54
+ },
55
+ async ({ branch }) => {
56
+ try {
57
+ const result = await new WorktreeManager(services.process).cleanup(services.cwd, branch);
58
+ logger.info({ tool: TOOL_NAME, branch }, "worktree removed, branch kept");
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: `Removed worktree for ${branch}. Branch kept. To merge: ${result.mergeSuggestion}`
64
+ }
65
+ ],
66
+ structuredContent: { ...result }
67
+ };
68
+ } catch (err) {
69
+ const message = err instanceof WorktreeError ? err.message : String(err);
70
+ logger.info({ tool: TOOL_NAME, branch, err: message }, "worktree cleanup failed");
71
+ return { isError: true, content: [{ type: "text", text: message }] };
72
+ }
73
+ }
74
+ );
75
+ }
76
+
77
+ // src/tools/create-worktree.ts
78
+ import { GitDetector, WorktreeError as WorktreeError2, WorktreeManager as WorktreeManager2 } from "@chamba/core";
79
+ import { z as z2 } from "zod";
80
+ var TOOL_NAME2 = "chamba_create_worktree";
81
+ var NOT_GIT = "Not a git repo, worktree skipped. Worker should use main cwd.";
82
+ var DESCRIPTION2 = "Create an isolated git worktree for a task/worker under `.chamba/worktrees/<task>/<worker>/` on branch `chamba/<date>-<task>/<worker>`. If the directory is not a git repo, returns an error and the worker should use the main cwd instead.";
83
+ function registerCreateWorktree(server, logger, services) {
84
+ server.registerTool(
85
+ TOOL_NAME2,
86
+ {
87
+ title: "Create worktree",
88
+ description: DESCRIPTION2,
89
+ inputSchema: {
90
+ taskSlug: z2.string().describe("Short task identifier (slugified for git)."),
91
+ workerId: z2.string().describe('Worker identifier, e.g. "implementer".'),
92
+ baseBranch: z2.string().optional().describe("Branch to base the worktree on.")
93
+ }
94
+ },
95
+ async ({ taskSlug, workerId, baseBranch }) => {
96
+ const isGit = await new GitDetector(services.process).isGitRepo(services.cwd);
97
+ if (!isGit) {
98
+ logger.info({ tool: TOOL_NAME2 }, "not a git repo");
99
+ return { isError: true, content: [{ type: "text", text: NOT_GIT }] };
100
+ }
101
+ try {
102
+ const handle = await new WorktreeManager2(services.process).create({
103
+ root: services.cwd,
104
+ taskSlug,
105
+ workerId,
106
+ date: services.clock.today(),
107
+ baseBranch
108
+ });
109
+ logger.info({ tool: TOOL_NAME2, branch: handle.branch }, "worktree created");
110
+ return {
111
+ content: [
112
+ { type: "text", text: `Created worktree at ${handle.path} on branch ${handle.branch}` }
113
+ ],
114
+ structuredContent: { ...handle }
115
+ };
116
+ } catch (err) {
117
+ const message = err instanceof WorktreeError2 ? err.message : String(err);
118
+ logger.info({ tool: TOOL_NAME2, err: message }, "worktree create failed");
119
+ return { isError: true, content: [{ type: "text", text: message }] };
120
+ }
121
+ }
122
+ );
123
+ }
124
+
125
+ // src/tools/generate-plan.ts
126
+ import { generatePlanTemplate, WorkspaceScanner } from "@chamba/core";
127
+ import { z as z3 } from "zod";
128
+ var TOOL_NAME3 = "chamba_generate_plan";
129
+ var DESCRIPTION3 = "Generate a structured plan TEMPLATE (not a finished plan) for a task: goal, acceptance criteria, subtasks with suggested workers, risks, and files likely touched. The editor model fills the placeholders, then calls chamba_review_plan. No LLM is used here.";
130
+ function registerGeneratePlan(server, logger, services) {
131
+ server.registerTool(
132
+ TOOL_NAME3,
133
+ {
134
+ title: "Generate plan",
135
+ description: DESCRIPTION3,
136
+ inputSchema: {
137
+ task: z3.string().describe("What the plan should accomplish."),
138
+ context: z3.string().optional().describe("Context from chamba_load_context, if any.")
139
+ }
140
+ },
141
+ async ({ task, context }) => {
142
+ const workspace = await new WorkspaceScanner(services.fs).scan(services.cwd);
143
+ const template = generatePlanTemplate({ task, context, workspace });
144
+ logger.info({ tool: TOOL_NAME3 }, "plan template generated");
145
+ return { content: [{ type: "text", text: template }] };
146
+ }
147
+ );
148
+ }
149
+
150
+ // src/tools/list-worktrees.ts
151
+ import { WorktreeManager as WorktreeManager3 } from "@chamba/core";
152
+ var TOOL_NAME4 = "chamba_list_worktrees";
153
+ var DESCRIPTION4 = "List the git worktrees in the current repo (path, HEAD, branch).";
154
+ function registerListWorktrees(server, logger, services) {
155
+ server.registerTool(
156
+ TOOL_NAME4,
157
+ { title: "List worktrees", description: DESCRIPTION4, inputSchema: {} },
158
+ async () => {
159
+ const worktrees = await new WorktreeManager3(services.process).list(services.cwd);
160
+ logger.info({ tool: TOOL_NAME4, count: worktrees.length }, "listed worktrees");
161
+ const text = worktrees.length === 0 ? "No worktrees found (or not a git repo)." : worktrees.map((w) => `- ${w.path}${w.branch ? ` [${w.branch}]` : ""}`).join("\n");
162
+ return {
163
+ content: [{ type: "text", text }],
164
+ structuredContent: { worktrees }
165
+ };
166
+ }
167
+ );
168
+ }
169
+
170
+ // src/tools/load-context.ts
171
+ import { ContextBuilder, ObsidianDetector, WorkspaceScanner as WorkspaceScanner2 } from "@chamba/core";
172
+ import { z as z4 } from "zod";
173
+ var TOOL_NAME5 = "chamba_load_context";
174
+ var DESCRIPTION5 = "Load context for a task: a summary of the workspace plus, when an Obsidian vault is available, the notes most relevant to the task (keyword search). Returns a markdown block the model can reason over before planning.";
175
+ function registerLoadContext(server, logger, services) {
176
+ server.registerTool(
177
+ TOOL_NAME5,
178
+ {
179
+ title: "Load context",
180
+ description: DESCRIPTION5,
181
+ inputSchema: {
182
+ task: z4.string().describe("The task you are about to work on."),
183
+ includeObsidian: z4.boolean().optional().describe("Search the Obsidian vault for relevant notes (default true).")
184
+ }
185
+ },
186
+ async ({ task, includeObsidian }) => {
187
+ const workspace = await new WorkspaceScanner2(services.fs).scan(services.cwd);
188
+ let vaultPath;
189
+ if (includeObsidian !== false) {
190
+ const detection = await new ObsidianDetector(services.fs).detect({
191
+ explicitPath: services.obsidianVaultPath,
192
+ searchRoots: obsidianSearchRoots(services)
193
+ });
194
+ if (detection.found) vaultPath = detection.path;
195
+ }
196
+ const built = await new ContextBuilder(services.fs).build({ workspace, task, vaultPath });
197
+ logger.info(
198
+ { tool: TOOL_NAME5, vault: vaultPath ?? null, notes: built.relevantNotes.length },
199
+ "context built"
200
+ );
201
+ return { content: [{ type: "text", text: built.context }] };
202
+ }
203
+ );
204
+ }
205
+
206
+ // src/tools/recall.ts
207
+ import { FilesystemMemoryStore } from "@chamba/core";
208
+ import { z as z5 } from "zod";
209
+ var TOOL_NAME6 = "chamba_recall";
210
+ var DESCRIPTION6 = "Search persisted memories (case-insensitive substring over key, tags and content) and return the matches with their paths and content.";
211
+ function registerRecall(server, logger, services) {
212
+ server.registerTool(
213
+ TOOL_NAME6,
214
+ {
215
+ title: "Recall",
216
+ description: DESCRIPTION6,
217
+ inputSchema: {
218
+ query: z5.string().describe("Keywords to search for.")
219
+ }
220
+ },
221
+ async ({ query }) => {
222
+ const store = new FilesystemMemoryStore(services.fs, services.clock, services.cwd);
223
+ const matches = await store.recall(query);
224
+ logger.info({ tool: TOOL_NAME6, query, matches: matches.length }, "recall");
225
+ const text = matches.length === 0 ? `No memories matched "${query}".` : matches.map((m) => `### ${m.key} (\`${m.path}\`)
226
+ ${m.content}`).join("\n\n");
227
+ return {
228
+ content: [{ type: "text", text }],
229
+ structuredContent: {
230
+ matches: matches.map((m) => ({
231
+ key: m.key,
232
+ path: m.path,
233
+ tags: m.tags,
234
+ content: m.content
235
+ }))
236
+ }
237
+ };
238
+ }
239
+ );
240
+ }
241
+
242
+ // src/tools/remember.ts
243
+ import { FilesystemMemoryStore as FilesystemMemoryStore2 } from "@chamba/core";
244
+ import { z as z6 } from "zod";
245
+ var TOOL_NAME7 = "chamba_remember";
246
+ var DESCRIPTION7 = "Persist a piece of knowledge across sessions as an editable markdown file under `.chamba/memory/<key>.md`. Re-remembering an existing key appends a timestamped section instead of overwriting.";
247
+ function registerRemember(server, logger, services) {
248
+ server.registerTool(
249
+ TOOL_NAME7,
250
+ {
251
+ title: "Remember",
252
+ description: DESCRIPTION7,
253
+ inputSchema: {
254
+ key: z6.string().describe('Short identifier, e.g. "auth-decisions".'),
255
+ content: z6.string().describe("What to remember (markdown)."),
256
+ tags: z6.array(z6.string()).optional().describe("Optional tags for search.")
257
+ }
258
+ },
259
+ async ({ key, content, tags }) => {
260
+ const store = new FilesystemMemoryStore2(services.fs, services.clock, services.cwd);
261
+ const memory = await store.remember({ key, content, tags });
262
+ logger.info({ tool: TOOL_NAME7, key, path: memory.path }, "memory saved");
263
+ return {
264
+ content: [{ type: "text", text: `Saved memory '${key}' to ${memory.path}` }],
265
+ structuredContent: { saved: true, path: memory.path }
266
+ };
267
+ }
268
+ );
269
+ }
270
+
271
+ // src/tools/review-plan.ts
272
+ import { Reviewer, WorkspaceScanner as WorkspaceScanner3 } from "@chamba/core";
273
+ import { z as z7 } from "zod";
274
+ var TOOL_NAME8 = "chamba_review_plan";
275
+ var DESCRIPTION8 = "Review a plan with programmatic heuristics (NO LLM): checks for acceptance criteria, tests, subtasks with assigned workers, concrete descriptions, files outside the workspace, and risk assessment for sensitive areas. Returns { approved, issues, suggestions, riskFlags }.";
276
+ function registerReviewPlan(server, logger, services) {
277
+ server.registerTool(
278
+ TOOL_NAME8,
279
+ {
280
+ title: "Review plan",
281
+ description: DESCRIPTION8,
282
+ inputSchema: {
283
+ plan: z7.string().describe("The plan markdown to review."),
284
+ task: z7.string().describe("The task the plan is for."),
285
+ context: z7.string().optional().describe("Context from chamba_load_context, if any.")
286
+ },
287
+ outputSchema: {
288
+ approved: z7.boolean(),
289
+ issues: z7.array(
290
+ z7.object({
291
+ code: z7.string(),
292
+ severity: z7.enum(["error", "warning"]),
293
+ message: z7.string()
294
+ })
295
+ ),
296
+ suggestions: z7.array(z7.string()),
297
+ riskFlags: z7.array(z7.string())
298
+ }
299
+ },
300
+ async ({ plan, task, context }) => {
301
+ const workspace = await new WorkspaceScanner3(services.fs).scan(services.cwd);
302
+ const review = new Reviewer().review({ plan, task, context, workspace });
303
+ logger.info(
304
+ { tool: TOOL_NAME8, approved: review.approved, issues: review.issues.length },
305
+ "plan reviewed"
306
+ );
307
+ const verdict = review.approved ? "\u2705 approved" : "\u274C changes requested";
308
+ const errorLines = review.issues.map((i) => `- [${i.severity}] ${i.code}: ${i.message}`);
309
+ const summary = [
310
+ `Plan review: ${verdict}`,
311
+ review.issues.length > 0 ? `
312
+ Issues:
313
+ ${errorLines.join("\n")}` : "",
314
+ review.riskFlags.length > 0 ? `
315
+ Risk flags:
316
+ ${review.riskFlags.map((r) => `- ${r}`).join("\n")}` : "",
317
+ review.suggestions.length > 0 ? `
318
+ Suggestions:
319
+ ${review.suggestions.map((s) => `- ${s}`).join("\n")}` : ""
320
+ ].filter((s) => s.length > 0).join("\n");
321
+ const structuredContent = review;
322
+ return { content: [{ type: "text", text: summary }], structuredContent };
323
+ }
324
+ );
325
+ }
326
+
327
+ // src/tools/summarize-to-vault.ts
328
+ import { ObsidianDetector as ObsidianDetector2, VaultWriter } from "@chamba/core";
329
+ import { z as z8 } from "zod";
330
+ var TOOL_NAME9 = "chamba_summarize_to_vault";
331
+ var NO_VAULT_ERROR = "No Obsidian vault configured. Set CHAMBA_OBSIDIAN_VAULT_PATH or use the obsidian-mcp server";
332
+ var DESCRIPTION9 = "Write a structured summary note to the Obsidian vault under `proyectos/<date>-<slug>.md` with valid YAML frontmatter. Fails clearly if no vault is configured.";
333
+ function registerSummarizeToVault(server, logger, services) {
334
+ server.registerTool(
335
+ TOOL_NAME9,
336
+ {
337
+ title: "Summarize to vault",
338
+ description: DESCRIPTION9,
339
+ inputSchema: {
340
+ title: z8.string().describe("Note title."),
341
+ content: z8.string().describe("Markdown body (summary, plan, decisions, next steps)."),
342
+ projectSlug: z8.string().optional().describe("Optional slug for the filename.")
343
+ }
344
+ },
345
+ async ({ title, content, projectSlug }) => {
346
+ const detection = await new ObsidianDetector2(services.fs).detect({
347
+ explicitPath: services.obsidianVaultPath,
348
+ searchRoots: obsidianSearchRoots(services)
349
+ });
350
+ if (!detection.found || !detection.path) {
351
+ logger.info({ tool: TOOL_NAME9 }, "no vault configured");
352
+ return { isError: true, content: [{ type: "text", text: NO_VAULT_ERROR }] };
353
+ }
354
+ const writer = new VaultWriter(services.fs, services.clock);
355
+ const { notePath } = await writer.write({
356
+ vaultPath: detection.path,
357
+ title,
358
+ content,
359
+ projectSlug
360
+ });
361
+ logger.info({ tool: TOOL_NAME9, notePath }, "note written to vault");
362
+ return { content: [{ type: "text", text: `Wrote note to ${notePath}` }] };
363
+ }
364
+ );
365
+ }
366
+
367
+ // src/tools/workspace-init.ts
368
+ import {
369
+ joinPath,
370
+ renderWorkspaceMarkdown,
371
+ WORKSPACE_DIR,
372
+ WORKSPACE_RELATIVE_PATH,
373
+ WorkspaceScanner as WorkspaceScanner4
374
+ } from "@chamba/core";
375
+ import { z as z9 } from "zod";
376
+ var TOOL_NAME10 = "chamba_workspace_init";
377
+ var DESCRIPTION10 = "Scan the workspace and generate `.chamba/workspace.md` (description, languages, framework, conventions, active projects, folder map). Respects .gitignore/.dockerignore and never reads node_modules or binaries. If the file already exists it is NOT overwritten \u2014 its current contents are returned so the model/user decides what to do.";
378
+ function registerWorkspaceInit(server, logger, services) {
379
+ server.registerTool(
380
+ TOOL_NAME10,
381
+ {
382
+ title: "Init workspace",
383
+ description: DESCRIPTION10,
384
+ inputSchema: {
385
+ root: z9.string().optional().describe("Workspace root to scan. Defaults to the directory chamba runs in.")
386
+ }
387
+ },
388
+ async ({ root }) => {
389
+ const workspaceRoot = root ?? services.cwd;
390
+ const wsPath = joinPath(workspaceRoot, WORKSPACE_RELATIVE_PATH);
391
+ if (await services.fs.exists(wsPath)) {
392
+ const currentContents = await services.fs.readFile(wsPath);
393
+ logger.info({ tool: TOOL_NAME10, wsPath }, "workspace.md already exists, not overwriting");
394
+ return {
395
+ content: [
396
+ {
397
+ type: "text",
398
+ text: `\`${WORKSPACE_RELATIVE_PATH}\` already exists at ${wsPath}; not overwriting.
399
+
400
+ Current contents:
401
+
402
+ ${currentContents}`
403
+ }
404
+ ]
405
+ };
406
+ }
407
+ const scanner = new WorkspaceScanner4(services.fs);
408
+ const workspace = await scanner.scan(workspaceRoot);
409
+ const markdown = renderWorkspaceMarkdown(workspace);
410
+ await services.fs.mkdir(joinPath(workspaceRoot, WORKSPACE_DIR));
411
+ await services.fs.writeFile(wsPath, markdown);
412
+ logger.info(
413
+ { tool: TOOL_NAME10, wsPath, projects: workspace.projects.length },
414
+ "workspace.md created"
415
+ );
416
+ return {
417
+ content: [
418
+ {
419
+ type: "text",
420
+ text: `Created \`${WORKSPACE_RELATIVE_PATH}\` at ${wsPath}.
421
+
422
+ ${markdown}`
423
+ }
424
+ ]
425
+ };
426
+ }
427
+ );
428
+ }
429
+
430
+ // src/tools/workspace-reload.ts
431
+ import {
432
+ diffLines,
433
+ joinPath as joinPath2,
434
+ renderWorkspaceMarkdown as renderWorkspaceMarkdown2,
435
+ textsEqual,
436
+ WORKSPACE_RELATIVE_PATH as WORKSPACE_RELATIVE_PATH2,
437
+ WorkspaceScanner as WorkspaceScanner5
438
+ } from "@chamba/core";
439
+ var TOOL_NAME11 = "chamba_workspace_reload";
440
+ var DESCRIPTION11 = "Re-scan the workspace and return a diff against the current `.chamba/workspace.md`. NEVER overwrites the file \u2014 the user may have hand-edited it. The model decides whether to apply changes.";
441
+ function registerWorkspaceReload(server, logger, services) {
442
+ server.registerTool(
443
+ TOOL_NAME11,
444
+ {
445
+ title: "Reload workspace",
446
+ description: DESCRIPTION11,
447
+ inputSchema: {}
448
+ },
449
+ async () => {
450
+ const wsPath = joinPath2(services.cwd, WORKSPACE_RELATIVE_PATH2);
451
+ if (!await services.fs.exists(wsPath)) {
452
+ logger.info({ tool: TOOL_NAME11, wsPath }, "no workspace.md to reload");
453
+ return {
454
+ content: [
455
+ {
456
+ type: "text",
457
+ text: `No \`${WORKSPACE_RELATIVE_PATH2}\` found at ${wsPath}. Run chamba_workspace_init first.`
458
+ }
459
+ ]
460
+ };
461
+ }
462
+ const current = await services.fs.readFile(wsPath);
463
+ const scanner = new WorkspaceScanner5(services.fs);
464
+ const rescanned = renderWorkspaceMarkdown2(await scanner.scan(services.cwd));
465
+ if (textsEqual(current, rescanned)) {
466
+ logger.info({ tool: TOOL_NAME11, wsPath }, "workspace.md up to date");
467
+ return {
468
+ content: [
469
+ {
470
+ type: "text",
471
+ text: `\`${WORKSPACE_RELATIVE_PATH2}\` is up to date \u2014 no changes from a re-scan.`
472
+ }
473
+ ]
474
+ };
475
+ }
476
+ logger.info({ tool: TOOL_NAME11, wsPath }, "workspace.md differs from re-scan");
477
+ return {
478
+ content: [
479
+ {
480
+ type: "text",
481
+ text: `Re-scan differs from \`${WORKSPACE_RELATIVE_PATH2}\` (NOT applied \u2014 your edits are kept). Diff (\`-\` current, \`+\` re-scan):
482
+
483
+ \`\`\`diff
484
+ ${diffLines(current, rescanned)}
485
+ \`\`\``
486
+ }
487
+ ]
488
+ };
489
+ }
490
+ );
491
+ }
492
+
493
+ // src/tools/workspace-show.ts
494
+ import { joinPath as joinPath3, WORKSPACE_RELATIVE_PATH as WORKSPACE_RELATIVE_PATH3 } from "@chamba/core";
495
+ var TOOL_NAME12 = "chamba_workspace_show";
496
+ var DESCRIPTION12 = "Show the current workspace map. Reads `.chamba/workspace.md` from the workspace root and returns its contents. If no workspace file exists yet, says so \u2014 the model can then run chamba_workspace_init to create one.";
497
+ function registerWorkspaceShow(server, logger, services) {
498
+ server.registerTool(
499
+ TOOL_NAME12,
500
+ {
501
+ title: "Show workspace",
502
+ description: DESCRIPTION12,
503
+ inputSchema: {}
504
+ },
505
+ async () => {
506
+ const path = joinPath3(services.cwd, WORKSPACE_RELATIVE_PATH3);
507
+ try {
508
+ const contents = await services.fs.readFile(path);
509
+ logger.info({ tool: TOOL_NAME12, path }, "workspace.md read");
510
+ return { content: [{ type: "text", text: contents }] };
511
+ } catch {
512
+ logger.info({ tool: TOOL_NAME12, path }, "no workspace.md found");
513
+ return {
514
+ content: [
515
+ {
516
+ type: "text",
517
+ text: `No \`${WORKSPACE_RELATIVE_PATH3}\` found at ${path}. Run chamba_workspace_init to create one.`
518
+ }
519
+ ]
520
+ };
521
+ }
522
+ }
523
+ );
524
+ }
525
+
526
+ // src/server.ts
527
+ var SERVER_NAME = "chamba";
528
+ var SERVER_VERSION = "0.0.0";
529
+ function createServer(logger, services = createNodeServices()) {
530
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
531
+ registerWorkspaceShow(server, logger, services);
532
+ registerWorkspaceInit(server, logger, services);
533
+ registerWorkspaceReload(server, logger, services);
534
+ registerLoadContext(server, logger, services);
535
+ registerSummarizeToVault(server, logger, services);
536
+ registerGeneratePlan(server, logger, services);
537
+ registerReviewPlan(server, logger, services);
538
+ registerCreateWorktree(server, logger, services);
539
+ registerListWorktrees(server, logger, services);
540
+ registerCleanupWorktree(server, logger, services);
541
+ registerRemember(server, logger, services);
542
+ registerRecall(server, logger, services);
543
+ return server;
544
+ }
545
+
546
+ // src/main.ts
547
+ async function main() {
548
+ const logger = createLogger();
549
+ const server = createServer(logger);
550
+ const transport = new StdioServerTransport();
551
+ await server.connect(transport);
552
+ logger.info("chamba MCP server connected over stdio");
553
+ const shutdown = (signal) => {
554
+ logger.info({ signal }, "shutting down");
555
+ void server.close().finally(() => process.exit(0));
556
+ };
557
+ process.on("SIGINT", () => shutdown("SIGINT"));
558
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
559
+ }
560
+ main().catch((err) => {
561
+ process.stderr.write(`chamba-mcp fatal: ${err.message}
562
+ `);
563
+ process.exit(1);
564
+ });
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@chamba/mcp",
3
+ "version": "0.1.0",
4
+ "description": "chamba MCP server — orchestration, workspace, worktree and Obsidian tools for any MCP-capable editor",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/main.js",
8
+ "bin": {
9
+ "chamba-mcp": "./bin/chamba-mcp"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "bin"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/thelord07/chamba.git",
18
+ "directory": "packages/mcp"
19
+ },
20
+ "homepage": "https://github.com/thelord07/chamba#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/thelord07/chamba/issues"
23
+ },
24
+ "keywords": [
25
+ "chamba",
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "ai",
29
+ "cursor",
30
+ "claude-code",
31
+ "obsidian",
32
+ "git-worktree",
33
+ "orchestrator"
34
+ ],
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.12.0",
37
+ "pino": "^9.0.0",
38
+ "zod": "^3.23.0",
39
+ "@chamba/core": "0.1.0",
40
+ "@chamba/adapters": "0.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "tsup": "^8.0.0",
45
+ "typescript": "^5.6.0",
46
+ "vitest": "^2.0.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "build": "tsup src/main.ts --format esm --clean",
53
+ "test": "vitest run",
54
+ "typecheck": "tsc --noEmit"
55
+ }
56
+ }