@vibecodetown/mcp-server 2.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 +269 -0
- package/build/auth/gate.js +225 -0
- package/build/auth/index.js +55 -0
- package/build/auth/public_key.js +27 -0
- package/build/auth/token_cache.js +122 -0
- package/build/auth/token_verifier.js +103 -0
- package/build/bootstrap/doctor.js +115 -0
- package/build/bootstrap/installer.js +673 -0
- package/build/bootstrap/lock.js +37 -0
- package/build/bootstrap/platform.js +26 -0
- package/build/bootstrap/registry.js +37 -0
- package/build/cache/index.js +147 -0
- package/build/cli.js +101 -0
- package/build/contracts.js +22 -0
- package/build/control_plane/gate.js +161 -0
- package/build/control_plane/index.js +6 -0
- package/build/dx/activity.js +139 -0
- package/build/engine.js +106 -0
- package/build/errors.js +171 -0
- package/build/generated/activate_input.js +2 -0
- package/build/generated/activate_output.js +57 -0
- package/build/generated/advisory_review_input.js +2 -0
- package/build/generated/advisory_review_output.js +35 -0
- package/build/generated/auth_token_file.js +2 -0
- package/build/generated/briefing_input.js +2 -0
- package/build/generated/briefing_output.js +2 -0
- package/build/generated/clinic_bridge_file.js +13 -0
- package/build/generated/contracts_bundle_info.js +5 -0
- package/build/generated/create_work_order_input.js +2 -0
- package/build/generated/create_work_order_output.js +2 -0
- package/build/generated/current_work_order_file.js +2 -0
- package/build/generated/doctor_input.js +2 -0
- package/build/generated/doctor_output.js +24 -0
- package/build/generated/execution_result.js +2 -0
- package/build/generated/execution_task.js +2 -0
- package/build/generated/export_output_input.js +2 -0
- package/build/generated/export_output_output.js +2 -0
- package/build/generated/finalize_work_input.js +2 -0
- package/build/generated/finalize_work_output.js +2 -0
- package/build/generated/gate_input.js +2 -0
- package/build/generated/gate_output.js +2 -0
- package/build/generated/gate_result_v1.js +2 -0
- package/build/generated/get_decision_input.js +2 -0
- package/build/generated/get_decision_output.js +13 -0
- package/build/generated/handoff_to_clinic.js +2 -0
- package/build/generated/index.js +75 -0
- package/build/generated/inspect_code_input.js +2 -0
- package/build/generated/inspect_code_output.js +13 -0
- package/build/generated/memory_retrieve_output.js +2 -0
- package/build/generated/memory_state_file.js +2 -0
- package/build/generated/memory_status_input.js +2 -0
- package/build/generated/memory_status_output.js +13 -0
- package/build/generated/memory_sync_input.js +2 -0
- package/build/generated/memory_sync_output.js +13 -0
- package/build/generated/plugin_result.js +2 -0
- package/build/generated/react_perf_check_patterns_input.js +2 -0
- package/build/generated/react_perf_check_patterns_output.js +2 -0
- package/build/generated/react_perf_generate_report_input.js +2 -0
- package/build/generated/react_perf_generate_report_output.js +2 -0
- package/build/generated/repair_plan_input.js +2 -0
- package/build/generated/repair_plan_output.js +2 -0
- package/build/generated/run_app_input.js +2 -0
- package/build/generated/run_app_output.js +2 -0
- package/build/generated/run_state_file.js +13 -0
- package/build/generated/scaffold_input.js +2 -0
- package/build/generated/scaffold_output.js +2 -0
- package/build/generated/search_oss_input.js +2 -0
- package/build/generated/search_oss_output.js +2 -0
- package/build/generated/selection_validation_result.js +2 -0
- package/build/generated/signal_agent_input.js +2 -0
- package/build/generated/spec_high_ask_queue_items_file.js +2 -0
- package/build/generated/spec_high_clinic_bridge_output.js +2 -0
- package/build/generated/spec_high_decision_draft_output.js +2 -0
- package/build/generated/spec_high_validate_output.js +2 -0
- package/build/generated/status_input.js +2 -0
- package/build/generated/status_output.js +2 -0
- package/build/generated/submit_decision_input.js +2 -0
- package/build/generated/submit_decision_output.js +2 -0
- package/build/generated/tool_error_output.js +2 -0
- package/build/generated/undo_last_task_input.js +2 -0
- package/build/generated/undo_last_task_output.js +2 -0
- package/build/generated/update_input.js +2 -0
- package/build/generated/update_output.js +2 -0
- package/build/generated/vibe_pm_inspection_result.js +2 -0
- package/build/generated/vibe_pm_report_markdown.js +2 -0
- package/build/generated/vibe_pm_verdict.js +2 -0
- package/build/generated/vibe_repo_config.js +2 -0
- package/build/generated/vibecoding_helper_answer_output.js +2 -0
- package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
- package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
- package/build/generated/work_order_v1.js +2 -0
- package/build/generated/zoekt_evidence_input.js +2 -0
- package/build/generated/zoekt_evidence_output.js +2 -0
- package/build/index.js +111 -0
- package/build/legacy_alias.js +65 -0
- package/build/local-mode/bash.js +61 -0
- package/build/local-mode/config.js +171 -0
- package/build/local-mode/git.js +33 -0
- package/build/local-mode/init.js +110 -0
- package/build/local-mode/paths.js +24 -0
- package/build/local-mode/templates.js +856 -0
- package/build/local-mode/work-order.js +41 -0
- package/build/resources/index.js +246 -0
- package/build/security/input-validator.js +119 -0
- package/build/security/path-policy.js +289 -0
- package/build/security/sandbox.js +228 -0
- package/build/tools/react_perf/check_patterns.js +172 -0
- package/build/tools/react_perf/generate_report.js +337 -0
- package/build/tools/react_perf/index.js +119 -0
- package/build/tools/react_perf/rules/advanced.js +325 -0
- package/build/tools/react_perf/rules/async.js +104 -0
- package/build/tools/react_perf/rules/bundle.js +101 -0
- package/build/tools/react_perf/rules/client.js +186 -0
- package/build/tools/react_perf/rules/index.js +74 -0
- package/build/tools/react_perf/rules/js.js +148 -0
- package/build/tools/react_perf/rules/rendering.js +166 -0
- package/build/tools/react_perf/rules/rerender.js +161 -0
- package/build/tools/react_perf/rules/server.js +141 -0
- package/build/tools/react_perf/types.js +127 -0
- package/build/tools/vibe_pm/activate.js +102 -0
- package/build/tools/vibe_pm/advisory_review.js +77 -0
- package/build/tools/vibe_pm/briefing.js +178 -0
- package/build/tools/vibe_pm/context.js +439 -0
- package/build/tools/vibe_pm/create_work_order.js +271 -0
- package/build/tools/vibe_pm/doc_status_gate.js +370 -0
- package/build/tools/vibe_pm/doctor.js +262 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
- package/build/tools/vibe_pm/export_output.js +135 -0
- package/build/tools/vibe_pm/finalize_work.js +393 -0
- package/build/tools/vibe_pm/gate.js +33 -0
- package/build/tools/vibe_pm/get_decision.js +281 -0
- package/build/tools/vibe_pm/index.js +593 -0
- package/build/tools/vibe_pm/inspect_code.js +828 -0
- package/build/tools/vibe_pm/intent/generator.js +294 -0
- package/build/tools/vibe_pm/intent/index.js +5 -0
- package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
- package/build/tools/vibe_pm/intent/types.js +70 -0
- package/build/tools/vibe_pm/intent/verifier.js +237 -0
- package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
- package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
- package/build/tools/vibe_pm/kce/preflight.js +232 -0
- package/build/tools/vibe_pm/local_memory.js +26 -0
- package/build/tools/vibe_pm/memory_status.js +82 -0
- package/build/tools/vibe_pm/memory_sync.js +134 -0
- package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
- package/build/tools/vibe_pm/modules/ensure.js +100 -0
- package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
- package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
- package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
- package/build/tools/vibe_pm/modules/repo_context.js +56 -0
- package/build/tools/vibe_pm/modules/research_v1.js +114 -0
- package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
- package/build/tools/vibe_pm/pm_language.js +222 -0
- package/build/tools/vibe_pm/repair_plan.js +199 -0
- package/build/tools/vibe_pm/run_app.js +597 -0
- package/build/tools/vibe_pm/run_app_podman.js +64 -0
- package/build/tools/vibe_pm/scaffold.js +550 -0
- package/build/tools/vibe_pm/search_oss.js +124 -0
- package/build/tools/vibe_pm/status.js +153 -0
- package/build/tools/vibe_pm/submit_decision.js +87 -0
- package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
- package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
- package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
- package/build/tools/vibe_pm/types.js +229 -0
- package/build/tools/vibe_pm/undo_last_task.js +163 -0
- package/build/tools/vibe_pm/update.js +146 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
- package/build/tools.js +269 -0
- package/build/version-check.js +239 -0
- package/build/vibe-cli.js +631 -0
- package/package.json +76 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { CurrentWorkOrderFileSchema } from "../generated/current_work_order_file.js";
|
|
4
|
+
export function readCurrentWorkOrder(filePath) {
|
|
5
|
+
if (!fs.existsSync(filePath))
|
|
6
|
+
return null;
|
|
7
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
8
|
+
return CurrentWorkOrderFileSchema.parse(raw);
|
|
9
|
+
}
|
|
10
|
+
export function createWorkOrderTemplate(topic) {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const ymd = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
13
|
+
const suffix = String(now.getTime() % 1000).padStart(3, "0");
|
|
14
|
+
return CurrentWorkOrderFileSchema.parse({
|
|
15
|
+
run_id: `wo-${ymd}-${suffix}`,
|
|
16
|
+
created_at: now.toISOString(),
|
|
17
|
+
topic: topic && topic.trim().length > 0 ? topic.trim() : "Work order created locally",
|
|
18
|
+
scope: {
|
|
19
|
+
include: ["src/**", "lib/**", "tests/**", "docs/**"],
|
|
20
|
+
exclude: [],
|
|
21
|
+
},
|
|
22
|
+
do_not_touch: ["config/**", ".env*", "secrets/**", "*.pem", "*.key"],
|
|
23
|
+
verify_criteria: [],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function writeCurrentWorkOrder(filePath, workOrder, opts) {
|
|
27
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
28
|
+
if (fs.existsSync(filePath) && !opts?.overwrite) {
|
|
29
|
+
throw new Error(`Work order already exists: ${filePath} (use --force to overwrite)`);
|
|
30
|
+
}
|
|
31
|
+
fs.writeFileSync(filePath, JSON.stringify(workOrder, null, 2) + "\n", "utf-8");
|
|
32
|
+
}
|
|
33
|
+
export function archiveCurrentWorkOrder(workOrderFile, archiveDir) {
|
|
34
|
+
const wo = readCurrentWorkOrder(workOrderFile);
|
|
35
|
+
if (!wo)
|
|
36
|
+
return null;
|
|
37
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
38
|
+
const archivedPath = path.join(archiveDir, `${wo.run_id}.json`);
|
|
39
|
+
fs.renameSync(workOrderFile, archivedPath);
|
|
40
|
+
return { archivedPath };
|
|
41
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/resources/index.ts
|
|
2
|
+
// Resource templates for vibe:// URI scheme
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
5
|
+
import { join, resolve } from "path";
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Resource URI Templates
|
|
8
|
+
// ============================================================
|
|
9
|
+
/**
|
|
10
|
+
* vibe://project/{runId} - Project state resource
|
|
11
|
+
* vibe://project/{runId}/intent - Intent document resource
|
|
12
|
+
* vibe://project/{runId}/bridge - Verification bridge resource
|
|
13
|
+
* vibe://project/{runId}/decisions - Decisions history resource
|
|
14
|
+
*/
|
|
15
|
+
// ============================================================
|
|
16
|
+
// Path Resolution Utilities
|
|
17
|
+
// ============================================================
|
|
18
|
+
function getRunsDir() {
|
|
19
|
+
// Default to current working directory's runs folder
|
|
20
|
+
return resolve(process.cwd(), "runs");
|
|
21
|
+
}
|
|
22
|
+
function getRunPath(runId) {
|
|
23
|
+
return join(getRunsDir(), runId);
|
|
24
|
+
}
|
|
25
|
+
function readJsonFile(filePath) {
|
|
26
|
+
if (!existsSync(filePath)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const content = readFileSync(filePath, "utf-8");
|
|
30
|
+
return JSON.parse(content);
|
|
31
|
+
}
|
|
32
|
+
function readYamlOrJson(basePath, filename) {
|
|
33
|
+
const jsonPath = join(basePath, `${filename}.json`);
|
|
34
|
+
const yamlPath = join(basePath, `${filename}.yaml`);
|
|
35
|
+
if (existsSync(jsonPath)) {
|
|
36
|
+
return readJsonFile(jsonPath);
|
|
37
|
+
}
|
|
38
|
+
if (existsSync(yamlPath)) {
|
|
39
|
+
const content = readFileSync(yamlPath, "utf-8");
|
|
40
|
+
// Simple YAML parsing for our use case (key: value format)
|
|
41
|
+
// For complex YAML, would need yaml library
|
|
42
|
+
return { raw: content, format: "yaml" };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// ============================================================
|
|
47
|
+
// Resource Content Schemas
|
|
48
|
+
// ============================================================
|
|
49
|
+
const projectStateSchema = z.object({
|
|
50
|
+
run_id: z.string(),
|
|
51
|
+
project_id: z.string(),
|
|
52
|
+
phase: z.string(),
|
|
53
|
+
mode: z.string(),
|
|
54
|
+
created_at: z.string(),
|
|
55
|
+
decisions_made: z.number(),
|
|
56
|
+
decisions_pending: z.number(),
|
|
57
|
+
last_review_status: z.enum(["GO", "FIX", "BLOCK"]).optional()
|
|
58
|
+
});
|
|
59
|
+
const intentDocumentSchema = z.object({
|
|
60
|
+
version: z.string(),
|
|
61
|
+
headline: z.string(),
|
|
62
|
+
scope: z.object({
|
|
63
|
+
include: z.array(z.string()),
|
|
64
|
+
exclude: z.array(z.string())
|
|
65
|
+
}),
|
|
66
|
+
verify_criteria: z.array(z.string()),
|
|
67
|
+
decisions: z.array(z.object({
|
|
68
|
+
decision_id: z.string(),
|
|
69
|
+
title: z.string(),
|
|
70
|
+
chosen: z.string()
|
|
71
|
+
}))
|
|
72
|
+
});
|
|
73
|
+
const bridgeSchema = z.object({
|
|
74
|
+
version: z.string().optional(),
|
|
75
|
+
intent: z.object({
|
|
76
|
+
headline: z.string(),
|
|
77
|
+
scope: z.object({
|
|
78
|
+
include: z.array(z.string()),
|
|
79
|
+
exclude: z.array(z.string())
|
|
80
|
+
}).optional()
|
|
81
|
+
}).optional(),
|
|
82
|
+
verify: z.object({
|
|
83
|
+
criteria: z.array(z.string()),
|
|
84
|
+
commands: z.array(z.string()).optional()
|
|
85
|
+
}).optional()
|
|
86
|
+
});
|
|
87
|
+
async function handleProjectState(runId) {
|
|
88
|
+
const runPath = getRunPath(runId);
|
|
89
|
+
const statePath = join(runPath, "state.json");
|
|
90
|
+
const state = readJsonFile(statePath);
|
|
91
|
+
if (!state) {
|
|
92
|
+
return {
|
|
93
|
+
contents: [{
|
|
94
|
+
uri: `vibe://project/${runId}`,
|
|
95
|
+
mimeType: "application/json",
|
|
96
|
+
text: JSON.stringify({
|
|
97
|
+
error: "NOT_FOUND",
|
|
98
|
+
message: `Run "${runId}" not found`,
|
|
99
|
+
run_id: runId
|
|
100
|
+
}, null, 2)
|
|
101
|
+
}]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
contents: [{
|
|
106
|
+
uri: `vibe://project/${runId}`,
|
|
107
|
+
mimeType: "application/json",
|
|
108
|
+
text: JSON.stringify(state, null, 2)
|
|
109
|
+
}]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function handleProjectIntent(runId) {
|
|
113
|
+
const runPath = getRunPath(runId);
|
|
114
|
+
const intentDir = join(runPath, "intent");
|
|
115
|
+
// Try to find intent document
|
|
116
|
+
const intent = readYamlOrJson(intentDir, "intent") ||
|
|
117
|
+
readYamlOrJson(runPath, "intent");
|
|
118
|
+
if (!intent) {
|
|
119
|
+
return {
|
|
120
|
+
contents: [{
|
|
121
|
+
uri: `vibe://project/${runId}/intent`,
|
|
122
|
+
mimeType: "application/json",
|
|
123
|
+
text: JSON.stringify({
|
|
124
|
+
error: "NOT_FOUND",
|
|
125
|
+
message: `Intent document not found for run "${runId}"`,
|
|
126
|
+
run_id: runId
|
|
127
|
+
}, null, 2)
|
|
128
|
+
}]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
contents: [{
|
|
133
|
+
uri: `vibe://project/${runId}/intent`,
|
|
134
|
+
mimeType: "application/json",
|
|
135
|
+
text: JSON.stringify(intent, null, 2)
|
|
136
|
+
}]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function handleProjectBridge(runId) {
|
|
140
|
+
const runPath = getRunPath(runId);
|
|
141
|
+
const handoffDir = join(runPath, "handoff");
|
|
142
|
+
const bridge = readYamlOrJson(handoffDir, "clinic_bridge") ||
|
|
143
|
+
readYamlOrJson(runPath, "clinic_bridge");
|
|
144
|
+
if (!bridge) {
|
|
145
|
+
return {
|
|
146
|
+
contents: [{
|
|
147
|
+
uri: `vibe://project/${runId}/bridge`,
|
|
148
|
+
mimeType: "application/json",
|
|
149
|
+
text: JSON.stringify({
|
|
150
|
+
error: "NOT_FOUND",
|
|
151
|
+
message: `Bridge file not found for run "${runId}"`,
|
|
152
|
+
run_id: runId,
|
|
153
|
+
hint: "Run vibe_pm.create_work_order to generate bridge file"
|
|
154
|
+
}, null, 2)
|
|
155
|
+
}]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
contents: [{
|
|
160
|
+
uri: `vibe://project/${runId}/bridge`,
|
|
161
|
+
mimeType: "application/json",
|
|
162
|
+
text: JSON.stringify(bridge, null, 2)
|
|
163
|
+
}]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function handleProjectDecisions(runId) {
|
|
167
|
+
const runPath = getRunPath(runId);
|
|
168
|
+
const decisionsPath = join(runPath, "decisions.json");
|
|
169
|
+
const decisions = readJsonFile(decisionsPath);
|
|
170
|
+
if (!decisions) {
|
|
171
|
+
return {
|
|
172
|
+
contents: [{
|
|
173
|
+
uri: `vibe://project/${runId}/decisions`,
|
|
174
|
+
mimeType: "application/json",
|
|
175
|
+
text: JSON.stringify({
|
|
176
|
+
error: "NOT_FOUND",
|
|
177
|
+
message: `Decisions not found for run "${runId}"`,
|
|
178
|
+
run_id: runId,
|
|
179
|
+
decisions: []
|
|
180
|
+
}, null, 2)
|
|
181
|
+
}]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
contents: [{
|
|
186
|
+
uri: `vibe://project/${runId}/decisions`,
|
|
187
|
+
mimeType: "application/json",
|
|
188
|
+
text: JSON.stringify(decisions, null, 2)
|
|
189
|
+
}]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// ============================================================
|
|
193
|
+
// Resource Registration
|
|
194
|
+
// ============================================================
|
|
195
|
+
/**
|
|
196
|
+
* Register all vibe:// resource templates
|
|
197
|
+
*/
|
|
198
|
+
export function registerVibeResources(server) {
|
|
199
|
+
// List available runs as resources
|
|
200
|
+
server.resource("vibe-projects", "vibe://projects", { mimeType: "application/json", description: "List all available Vibe PM runs" }, async () => {
|
|
201
|
+
const runsDir = getRunsDir();
|
|
202
|
+
let runs = [];
|
|
203
|
+
if (existsSync(runsDir)) {
|
|
204
|
+
runs = readdirSync(runsDir, { withFileTypes: true })
|
|
205
|
+
.filter(d => d.isDirectory())
|
|
206
|
+
.map(d => d.name)
|
|
207
|
+
.sort()
|
|
208
|
+
.reverse(); // Latest first
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
contents: [{
|
|
212
|
+
uri: "vibe://projects",
|
|
213
|
+
mimeType: "application/json",
|
|
214
|
+
text: JSON.stringify({
|
|
215
|
+
runs_dir: runsDir,
|
|
216
|
+
runs: runs.map(runId => ({
|
|
217
|
+
run_id: runId,
|
|
218
|
+
uri: `vibe://project/${runId}`,
|
|
219
|
+
intent_uri: `vibe://project/${runId}/intent`,
|
|
220
|
+
bridge_uri: `vibe://project/${runId}/bridge`
|
|
221
|
+
}))
|
|
222
|
+
}, null, 2)
|
|
223
|
+
}]
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Register dynamic resources based on available runs
|
|
229
|
+
*
|
|
230
|
+
* Note: MCP SDK doesn't support resource templates directly.
|
|
231
|
+
* Resources are registered dynamically for each run found.
|
|
232
|
+
* Call this after runs are created to register new resources.
|
|
233
|
+
*/
|
|
234
|
+
export function registerVibeResourceTemplates(_server) {
|
|
235
|
+
// Resource templates are not supported in the current MCP SDK version.
|
|
236
|
+
// Dynamic resources should be registered using the resource() method
|
|
237
|
+
// when runs are discovered. This is a placeholder for future SDK support.
|
|
238
|
+
//
|
|
239
|
+
// To access run-specific resources, use:
|
|
240
|
+
// - vibe://projects to list all runs
|
|
241
|
+
// - Tools like vibe_pm.status to get run details
|
|
242
|
+
}
|
|
243
|
+
// ============================================================
|
|
244
|
+
// Export
|
|
245
|
+
// ============================================================
|
|
246
|
+
export { projectStateSchema, intentDocumentSchema, bridgeSchema, getRunsDir, getRunPath };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/security/input-validator.ts
|
|
2
|
+
// Input validation for tool inputs
|
|
3
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Input size limits for various fields
|
|
6
|
+
*/
|
|
7
|
+
export const INPUT_LIMITS = {
|
|
8
|
+
projectBrief: 50_000,
|
|
9
|
+
additionalInstructions: 10_000,
|
|
10
|
+
targetPathsCount: 100,
|
|
11
|
+
singlePathLength: 500,
|
|
12
|
+
customInput: 5_000,
|
|
13
|
+
decisionNote: 2_000,
|
|
14
|
+
runIdLength: 128,
|
|
15
|
+
projectIdLength: 128,
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Security-related error class
|
|
19
|
+
*/
|
|
20
|
+
export class SecurityError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
constructor(code, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.name = "SecurityError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate string length against limits
|
|
30
|
+
*
|
|
31
|
+
* @throws McpError if validation fails
|
|
32
|
+
*/
|
|
33
|
+
export function validateStringLength(value, field, fieldName) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return;
|
|
36
|
+
const limit = INPUT_LIMITS[field];
|
|
37
|
+
if (value.length > limit) {
|
|
38
|
+
throw inputTooLargeError(fieldName, limit, value.length);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate array size
|
|
43
|
+
*
|
|
44
|
+
* @throws McpError if validation fails
|
|
45
|
+
*/
|
|
46
|
+
export function validateArraySize(arr, maxSize, fieldName) {
|
|
47
|
+
if (arr === undefined)
|
|
48
|
+
return;
|
|
49
|
+
if (arr.length > maxSize) {
|
|
50
|
+
throw inputTooLargeError(fieldName, maxSize, arr.length);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate each string in an array for length
|
|
55
|
+
*/
|
|
56
|
+
export function validateStringArray(arr, maxLength, fieldName) {
|
|
57
|
+
if (arr === undefined)
|
|
58
|
+
return;
|
|
59
|
+
for (let i = 0; i < arr.length; i++) {
|
|
60
|
+
if (arr[i].length > maxLength) {
|
|
61
|
+
throw inputTooLargeError(`${fieldName}[${i}]`, maxLength, arr[i].length);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate common tool input fields
|
|
67
|
+
*
|
|
68
|
+
* @throws McpError if any validation fails
|
|
69
|
+
*/
|
|
70
|
+
export function validateToolInput(input) {
|
|
71
|
+
// Validate string fields
|
|
72
|
+
validateStringLength(input.project_brief, "projectBrief", "project_brief");
|
|
73
|
+
validateStringLength(input.additional_instructions, "additionalInstructions", "additional_instructions");
|
|
74
|
+
validateStringLength(input.note, "decisionNote", "note");
|
|
75
|
+
validateStringLength(input.run_id, "runIdLength", "run_id");
|
|
76
|
+
validateStringLength(input.project_id, "projectIdLength", "project_id");
|
|
77
|
+
validateStringLength(input.custom_input, "customInput", "custom_input");
|
|
78
|
+
// Validate target_paths array
|
|
79
|
+
if (input.target_paths) {
|
|
80
|
+
validateArraySize(input.target_paths, INPUT_LIMITS.targetPathsCount, "target_paths");
|
|
81
|
+
validateStringArray(input.target_paths, INPUT_LIMITS.singlePathLength, "target_paths");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validate briefing-specific input
|
|
86
|
+
*/
|
|
87
|
+
export function validateBriefingInput(input) {
|
|
88
|
+
validateStringLength(input.project_brief, "projectBrief", "project_brief");
|
|
89
|
+
validateStringLength(input.project_id, "projectIdLength", "project_id");
|
|
90
|
+
// project_brief is required and must not be empty
|
|
91
|
+
if (!input.project_brief || input.project_brief.trim().length === 0) {
|
|
92
|
+
throw new McpError(ErrorCode.InvalidParams, "[VALIDATION] project_brief is required and must not be empty");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate inspect_code-specific input
|
|
97
|
+
*/
|
|
98
|
+
export function validateInspectCodeInput(input) {
|
|
99
|
+
if (input.target_paths) {
|
|
100
|
+
validateArraySize(input.target_paths, INPUT_LIMITS.targetPathsCount, "target_paths");
|
|
101
|
+
validateStringArray(input.target_paths, INPUT_LIMITS.singlePathLength, "target_paths");
|
|
102
|
+
}
|
|
103
|
+
if (input.bridge_path) {
|
|
104
|
+
validateStringLength(input.bridge_path, "singlePathLength", "bridge_path");
|
|
105
|
+
}
|
|
106
|
+
if (input.run_id) {
|
|
107
|
+
validateStringLength(input.run_id, "runIdLength", "run_id");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Create InputTooLarge error
|
|
112
|
+
*/
|
|
113
|
+
export function inputTooLargeError(field, limit, actual) {
|
|
114
|
+
return new McpError(ErrorCode.InvalidParams, `[SECURITY] Input too large: ${field} exceeds limit (${actual} > ${limit})`, {
|
|
115
|
+
vibeCode: -32_062,
|
|
116
|
+
category: "SECURITY",
|
|
117
|
+
context: { field, limit, actual },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/security/path-policy.ts
|
|
2
|
+
// Path validation and do_not_touch policy enforcement
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { minimatch } from "minimatch";
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Vibe-SDS Path Zones (Standard Directory Structure)
|
|
9
|
+
// ============================================================
|
|
10
|
+
/**
|
|
11
|
+
* Path zones for AI workspace control
|
|
12
|
+
* - BLACK: System only - AI cannot read or write
|
|
13
|
+
* - RED: Protected - AI cannot modify (auto do_not_touch)
|
|
14
|
+
* - YELLOW: Caution - AI can modify with warnings
|
|
15
|
+
* - GREEN: Free zone - AI workspace
|
|
16
|
+
*/
|
|
17
|
+
export const PATH_ZONES = {
|
|
18
|
+
/** System state - AI cannot access */
|
|
19
|
+
BLACK: [".vibe", ".vibe/**"],
|
|
20
|
+
/** Protected config - auto do_not_touch */
|
|
21
|
+
RED: ["config/**", ".env", ".env.*", "*.pem", "*.key", "credentials.*"],
|
|
22
|
+
/** Documentation - AI can modify with caution */
|
|
23
|
+
YELLOW: ["docs/**", "README.md", "*.md"],
|
|
24
|
+
/** AI workspace - free to modify */
|
|
25
|
+
GREEN: ["src/**", "tests/**", "lib/**", "scripts/**"],
|
|
26
|
+
};
|
|
27
|
+
const CONTROL_PLANE_BLACKLIST = [
|
|
28
|
+
"vibecoding_helper/**",
|
|
29
|
+
"adapters/**",
|
|
30
|
+
"engines/**",
|
|
31
|
+
"schemas/**",
|
|
32
|
+
"fixtures/**",
|
|
33
|
+
"policy/**",
|
|
34
|
+
"runs/**",
|
|
35
|
+
"config/semgrep/**",
|
|
36
|
+
"config/gitleaks/**",
|
|
37
|
+
"docs/ssot/**",
|
|
38
|
+
"docs/DEV_SPEC/implemented/**",
|
|
39
|
+
"scripts/generate-contracts.sh",
|
|
40
|
+
"scripts/generate-contract-lock.py",
|
|
41
|
+
"schemas/contracts.version.json",
|
|
42
|
+
"schemas/contracts.lock.json",
|
|
43
|
+
];
|
|
44
|
+
let cachedControlPlaneRepo = null;
|
|
45
|
+
function isControlPlaneRepo() {
|
|
46
|
+
if (cachedControlPlaneRepo !== null)
|
|
47
|
+
return cachedControlPlaneRepo;
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const hasSpec = fs.existsSync(path.join(cwd, "vibecoding_helper.spec"));
|
|
50
|
+
const hasLayout = fs.existsSync(path.join(cwd, "vibecoding_helper")) && fs.existsSync(path.join(cwd, "adapters", "mcp-ts"));
|
|
51
|
+
cachedControlPlaneRepo = hasSpec || hasLayout;
|
|
52
|
+
return cachedControlPlaneRepo;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the zone for a given path
|
|
56
|
+
*/
|
|
57
|
+
export function getPathZone(relativePath) {
|
|
58
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
59
|
+
if (isControlPlaneRepo()) {
|
|
60
|
+
for (const pattern of CONTROL_PLANE_BLACKLIST) {
|
|
61
|
+
if (minimatch(normalized, pattern, { dot: true })) {
|
|
62
|
+
return "BLACK";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Check zones in priority order: BLACK > RED > YELLOW > GREEN
|
|
67
|
+
for (const [zone, patterns] of Object.entries(PATH_ZONES)) {
|
|
68
|
+
for (const pattern of patterns) {
|
|
69
|
+
if (minimatch(normalized, pattern, { dot: true })) {
|
|
70
|
+
return zone;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check if a path is in a forbidden zone (BLACK or RED)
|
|
78
|
+
*/
|
|
79
|
+
export function isPathForbidden(relativePath) {
|
|
80
|
+
const zone = getPathZone(relativePath);
|
|
81
|
+
if (zone === "BLACK") {
|
|
82
|
+
return {
|
|
83
|
+
forbidden: true,
|
|
84
|
+
zone,
|
|
85
|
+
reason: "이 경로는 시스템 전용 영역입니다. AI가 접근할 수 없습니다.",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (zone === "RED") {
|
|
89
|
+
return {
|
|
90
|
+
forbidden: true,
|
|
91
|
+
zone,
|
|
92
|
+
reason: "이 경로는 보호된 설정 파일입니다. 수정할 수 없습니다.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { forbidden: false, zone: zone ?? undefined };
|
|
96
|
+
}
|
|
97
|
+
export function validateWorkOrderPaths(relativePaths) {
|
|
98
|
+
const allowed = [];
|
|
99
|
+
const forbidden = [];
|
|
100
|
+
for (const p of relativePaths) {
|
|
101
|
+
const check = isPathForbidden(p);
|
|
102
|
+
if (check.forbidden && check.zone && check.reason) {
|
|
103
|
+
forbidden.push({ path: p, zone: check.zone, reason: check.reason });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
allowed.push(p);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
valid: forbidden.length === 0,
|
|
111
|
+
allowed,
|
|
112
|
+
forbidden,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// ============================================================
|
|
116
|
+
// Dangerous Patterns (system-level protection)
|
|
117
|
+
// ============================================================
|
|
118
|
+
/**
|
|
119
|
+
* Dangerous path patterns that should always be blocked
|
|
120
|
+
*/
|
|
121
|
+
const DANGEROUS_PATTERNS = [
|
|
122
|
+
/\.\./, // Path traversal
|
|
123
|
+
/^\/etc\//, // System config
|
|
124
|
+
/^\/usr\//, // System binaries
|
|
125
|
+
/^\/bin\//, // Binaries
|
|
126
|
+
/^\/sbin\//, // System binaries
|
|
127
|
+
/^\/var\/log\//, // System logs
|
|
128
|
+
/^\/root\//, // Root home
|
|
129
|
+
/^\/home\/[^/]+\/\./, // Hidden files in home
|
|
130
|
+
/^C:\\Windows/i, // Windows system
|
|
131
|
+
/^C:\\Program Files/i, // Windows programs
|
|
132
|
+
/\.env$/i, // Environment files
|
|
133
|
+
/credentials\.json$/i, // Credential files
|
|
134
|
+
/\.pem$/i, // Private keys
|
|
135
|
+
/\.key$/i, // Key files
|
|
136
|
+
/id_rsa/i, // SSH keys
|
|
137
|
+
];
|
|
138
|
+
/**
|
|
139
|
+
* Normalize a path relative to a base path
|
|
140
|
+
*
|
|
141
|
+
* - Resolves relative paths
|
|
142
|
+
* - Prevents path traversal attacks
|
|
143
|
+
* - Returns normalized absolute path
|
|
144
|
+
*
|
|
145
|
+
* @throws McpError if path traversal is detected
|
|
146
|
+
*/
|
|
147
|
+
export function normalizePath(inputPath, basePath) {
|
|
148
|
+
const pathApi = getPathApi(basePath);
|
|
149
|
+
// Reject obvious path traversal
|
|
150
|
+
if (inputPath.includes("..")) {
|
|
151
|
+
throw pathTraversalError(inputPath);
|
|
152
|
+
}
|
|
153
|
+
// Resolve the path
|
|
154
|
+
const resolved = pathApi.isAbsolute(inputPath)
|
|
155
|
+
? inputPath
|
|
156
|
+
: pathApi.resolve(basePath, inputPath);
|
|
157
|
+
// Normalize to remove any . or redundant separators
|
|
158
|
+
const normalized = pathApi.normalize(resolved);
|
|
159
|
+
// Verify the resolved path is within basePath
|
|
160
|
+
const normalizedBase = pathApi.resolve(basePath);
|
|
161
|
+
const relative = pathApi.relative(normalizedBase, normalized);
|
|
162
|
+
if (relative.startsWith("..") || pathApi.isAbsolute(relative)) {
|
|
163
|
+
throw pathTraversalError(inputPath);
|
|
164
|
+
}
|
|
165
|
+
// Check against dangerous patterns
|
|
166
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
167
|
+
if (pattern.test(normalized) || pattern.test(inputPath)) {
|
|
168
|
+
throw pathTraversalError(inputPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return normalized;
|
|
172
|
+
}
|
|
173
|
+
function getPathApi(basePath) {
|
|
174
|
+
// When tests pass POSIX-style base paths on Windows, path.resolve/relative will
|
|
175
|
+
// introduce drive letters (e.g., F:\home\...) and break determinism.
|
|
176
|
+
// Choose the path implementation based on the basePath style.
|
|
177
|
+
if (/^[A-Za-z]:[\\/]/.test(basePath) || basePath.includes("\\")) {
|
|
178
|
+
return path.win32;
|
|
179
|
+
}
|
|
180
|
+
if (basePath.startsWith("/")) {
|
|
181
|
+
return path.posix;
|
|
182
|
+
}
|
|
183
|
+
return path;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Check if a path matches any do_not_touch patterns
|
|
187
|
+
*/
|
|
188
|
+
export function matchesDoNotTouch(targetPath, patterns) {
|
|
189
|
+
for (const pattern of patterns) {
|
|
190
|
+
// Try minimatch glob matching
|
|
191
|
+
if (minimatch(targetPath, pattern, { dot: true, matchBase: true })) {
|
|
192
|
+
return pattern;
|
|
193
|
+
}
|
|
194
|
+
// Also check if pattern is a prefix (directory match)
|
|
195
|
+
const normalizedTarget = targetPath.replace(/\\/g, "/");
|
|
196
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
197
|
+
if (normalizedTarget.startsWith(normalizedPattern + "/") ||
|
|
198
|
+
normalizedTarget === normalizedPattern) {
|
|
199
|
+
return pattern;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Check multiple paths against do_not_touch patterns
|
|
206
|
+
*
|
|
207
|
+
* @returns Object with allowed paths and any violations
|
|
208
|
+
*/
|
|
209
|
+
export function checkDoNotTouch(targetPaths, doNotTouchPatterns, basePath) {
|
|
210
|
+
const allowed = [];
|
|
211
|
+
const violations = [];
|
|
212
|
+
const pathApi = getPathApi(basePath);
|
|
213
|
+
for (const targetPath of targetPaths) {
|
|
214
|
+
try {
|
|
215
|
+
const normalized = normalizePath(targetPath, basePath);
|
|
216
|
+
const relativePath = pathApi.relative(basePath, normalized);
|
|
217
|
+
const matchedPattern = matchesDoNotTouch(relativePath, doNotTouchPatterns);
|
|
218
|
+
if (matchedPattern) {
|
|
219
|
+
violations.push({
|
|
220
|
+
path: targetPath,
|
|
221
|
+
pattern: matchedPattern,
|
|
222
|
+
normalizedPath: normalized,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
allowed.push(normalized);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
// Path normalization failed (traversal attack)
|
|
231
|
+
violations.push({
|
|
232
|
+
path: targetPath,
|
|
233
|
+
pattern: "PATH_TRAVERSAL",
|
|
234
|
+
normalizedPath: targetPath,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { allowed, violations };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Pre-execution check that throws if any violations are found
|
|
242
|
+
*
|
|
243
|
+
* @throws McpError if any path violates do_not_touch or is a traversal attempt
|
|
244
|
+
*/
|
|
245
|
+
export function preExecutionCheck(targetPaths, doNotTouchPatterns, basePath) {
|
|
246
|
+
const { allowed, violations } = checkDoNotTouch(targetPaths, doNotTouchPatterns, basePath);
|
|
247
|
+
if (violations.length > 0) {
|
|
248
|
+
const firstViolation = violations[0];
|
|
249
|
+
if (firstViolation.pattern === "PATH_TRAVERSAL") {
|
|
250
|
+
throw pathTraversalError(firstViolation.path);
|
|
251
|
+
}
|
|
252
|
+
throw doNotTouchError(firstViolation.path, firstViolation.pattern);
|
|
253
|
+
}
|
|
254
|
+
return allowed;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Validate a single path without do_not_touch check (just traversal protection)
|
|
258
|
+
*/
|
|
259
|
+
export function validatePath(inputPath, basePath) {
|
|
260
|
+
return normalizePath(inputPath, basePath);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Validate multiple paths and return normalized versions
|
|
264
|
+
*/
|
|
265
|
+
export function validatePaths(inputPaths, basePath) {
|
|
266
|
+
return inputPaths.map((p) => normalizePath(p, basePath));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Create path traversal error
|
|
270
|
+
*/
|
|
271
|
+
export function pathTraversalError(inputPath) {
|
|
272
|
+
return new McpError(ErrorCode.InvalidParams, `[SECURITY] Path traversal detected: "${inputPath}"`, {
|
|
273
|
+
vibeCode: -32_061,
|
|
274
|
+
category: "SECURITY",
|
|
275
|
+
context: { path: inputPath },
|
|
276
|
+
recovery: "Use paths relative to the project directory without '..' sequences",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Create do_not_touch violation error
|
|
281
|
+
*/
|
|
282
|
+
export function doNotTouchError(inputPath, pattern) {
|
|
283
|
+
return new McpError(ErrorCode.InvalidParams, `[SECURITY] Path "${inputPath}" matches do_not_touch pattern "${pattern}"`, {
|
|
284
|
+
vibeCode: -32_063,
|
|
285
|
+
category: "SECURITY",
|
|
286
|
+
context: { path: inputPath, pattern },
|
|
287
|
+
recovery: "This path is protected and cannot be modified. Choose a different target.",
|
|
288
|
+
});
|
|
289
|
+
}
|