@scopelock/mcp 0.1.0-beta.1

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 Alexander (Daewooox)
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,14 @@
1
+ # @scopelock/mcp
2
+
3
+ The narrow stdio MCP server for
4
+ [ScopeLock](https://github.com/Daewooox/ScopeLock). It exposes deterministic
5
+ scope conflict, scheduling, and drift-verification tools.
6
+
7
+ The package has not been published to npm yet. Until the first beta release,
8
+ build the repository and run `node packages/mcp/dist/index.js` from source.
9
+ After publication, the intended command is `npx --yes @scopelock/mcp@beta`.
10
+
11
+ The server is pinned to its startup repository and does not provide a generic
12
+ agent runtime.
13
+
14
+ MIT licensed. Requires Node.js 22 or newer.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createScopeLockMcpServer } from "./tools.js";
4
+ async function main() {
5
+ const server = createScopeLockMcpServer();
6
+ await server.connect(new StdioServerTransport());
7
+ }
8
+ main().catch((error) => {
9
+ const message = error instanceof Error ? error.message : String(error);
10
+ process.stderr.write(`ScopeLock MCP server failed: ${message}\n`);
11
+ process.exitCode = 1;
12
+ });
@@ -0,0 +1,64 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type TaskScope } from "@scopelock/core";
3
+ export declare const SERVER_NAME = "scopelock";
4
+ export declare const SERVER_VERSION = "0.1.0";
5
+ export declare function planParallelTool(input: {
6
+ plan: unknown;
7
+ includeReadHazards?: boolean;
8
+ }, serverRepoRoot?: string): Promise<{
9
+ planId: string;
10
+ waves: string[][];
11
+ conflicts: import("@scopelock/core").ScopeConflict[];
12
+ cycles: string[][];
13
+ }>;
14
+ export declare function scopesConflictTool(input: {
15
+ a: TaskScope;
16
+ b: TaskScope;
17
+ }): {
18
+ conflict: boolean;
19
+ detail: import("@scopelock/core").ScopeConflict | null;
20
+ };
21
+ export declare function checkDriftTool(input?: {
22
+ base?: string;
23
+ }, serverRepoRoot?: string): Promise<{
24
+ ok: boolean;
25
+ reportPath: string;
26
+ report: {
27
+ schemaVersion: 1;
28
+ contractId: string;
29
+ checkedAt: string;
30
+ repoMode: "normal" | "degraded" | "refused";
31
+ repoState: {
32
+ kind: "clean";
33
+ } | {
34
+ kind: "merge";
35
+ } | {
36
+ kind: "rebase";
37
+ } | {
38
+ kind: "cherry-pick";
39
+ } | {
40
+ kind: "revert";
41
+ } | {
42
+ kind: "am";
43
+ } | {
44
+ kind: "bisect";
45
+ };
46
+ changedFiles: {
47
+ path: string;
48
+ previousPath: string | null;
49
+ status: "modified" | "added" | "deleted" | "renamed" | "copied" | "untracked" | "conflicted";
50
+ stage: "untracked" | "conflicted" | "staged" | "unstaged";
51
+ isBinary: boolean;
52
+ insertions: number;
53
+ deletions: number;
54
+ sizeBytes: number;
55
+ }[];
56
+ violations: {
57
+ type: "outside_scope" | "forbidden_path" | "missing_tests" | "high_risk_file" | "repo_state" | "repo_mode";
58
+ path: string | null;
59
+ message: string;
60
+ }[];
61
+ contractIds?: string[] | undefined;
62
+ };
63
+ }>;
64
+ export declare function createScopeLockMcpServer(repoRoot?: string): McpServer;
package/dist/tools.js ADDED
@@ -0,0 +1,167 @@
1
+ import { readFile, realpath } from "node:fs/promises";
2
+ import { realpathSync } from "node:fs";
3
+ import { isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { approvedContractSchema, buildConflictGraph, buildDriftReport, collectChangedFiles, commitExists, driftReportFileName, findRepoRoot, getActiveContractId, loadContract, schedule, schedulePlanSchema, scopelockConfigSchema, scopelockPaths, scopesConflict, verifyApprovalSeal, writeJsonAtomic, } from "@scopelock/core";
6
+ import { join } from "node:path";
7
+ import { z } from "zod/v4";
8
+ export const SERVER_NAME = "scopelock";
9
+ export const SERVER_VERSION = "0.1.0";
10
+ const taskScopeInputSchema = z.object({
11
+ id: z.string().min(1),
12
+ planned: z.array(z.string().min(1)),
13
+ forbidden: z.array(z.string().min(1)).default([]),
14
+ read: z.array(z.string().min(1)).default([]),
15
+ });
16
+ const planParallelInputSchema = {
17
+ plan: z.unknown().describe("A ScopeLock schedule plan JSON object."),
18
+ includeReadHazards: z.boolean().optional().default(false),
19
+ };
20
+ const scopesConflictInputSchema = {
21
+ a: taskScopeInputSchema,
22
+ b: taskScopeInputSchema,
23
+ };
24
+ const checkDriftInputSchema = {
25
+ base: z.string().min(1).optional(),
26
+ };
27
+ function jsonContent(value) {
28
+ return {
29
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
30
+ structuredContent: value,
31
+ };
32
+ }
33
+ function resolveFromRoot(repoRoot, path) {
34
+ if (isAbsolute(path)) {
35
+ throw new Error("contract paths must be relative to the MCP server repo root");
36
+ }
37
+ const resolved = resolve(repoRoot, path);
38
+ const rel = relative(repoRoot, resolved);
39
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
40
+ throw new Error("contract path escapes the MCP server repo root");
41
+ }
42
+ return resolved;
43
+ }
44
+ async function readJsonFile(path) {
45
+ return JSON.parse(await readFile(path, "utf8"));
46
+ }
47
+ async function loadTaskScope(repoRoot, task) {
48
+ const contractPath = await realpath(resolveFromRoot(repoRoot, task.contract));
49
+ const rel = relative(repoRoot, contractPath);
50
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
51
+ throw new Error("contract path resolves outside the MCP server repo root");
52
+ }
53
+ const raw = await readJsonFile(contractPath);
54
+ const contract = approvedContractSchema.parse(raw);
55
+ return scopeFromContract(task.id, contract);
56
+ }
57
+ function scopeFromContract(id, contract) {
58
+ return {
59
+ id,
60
+ planned: contract.scope.plannedPathPatterns,
61
+ forbidden: contract.scope.forbiddenPathPatterns,
62
+ read: contract.scope.readPathPatterns,
63
+ };
64
+ }
65
+ async function loadConfig(paths) {
66
+ try {
67
+ const raw = await readFile(paths.configPath, "utf8");
68
+ return scopelockConfigSchema.parse(JSON.parse(raw));
69
+ }
70
+ catch (error) {
71
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
72
+ return scopelockConfigSchema.parse({ schemaVersion: 1 });
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+ function requireRepoRoot(candidate) {
78
+ const root = findRepoRoot(candidate ?? process.cwd());
79
+ if (root === null) {
80
+ throw new Error("ScopeLock MCP tools must run inside a git repository");
81
+ }
82
+ return realpathSync(root);
83
+ }
84
+ export async function planParallelTool(input, serverRepoRoot) {
85
+ const repoRoot = requireRepoRoot(serverRepoRoot);
86
+ const plan = schedulePlanSchema.parse(input.plan);
87
+ const scopes = [];
88
+ for (const task of plan.tasks) {
89
+ scopes.push(await loadTaskScope(repoRoot, task));
90
+ }
91
+ const graph = buildConflictGraph(scopes, { readHazards: input.includeReadHazards === true });
92
+ const result = schedule(graph);
93
+ return {
94
+ planId: plan.planId,
95
+ waves: result.waves,
96
+ conflicts: graph.conflicts,
97
+ cycles: result.cycles,
98
+ };
99
+ }
100
+ export function scopesConflictTool(input) {
101
+ const a = taskScopeInputSchema.parse(input.a);
102
+ const b = taskScopeInputSchema.parse(input.b);
103
+ const conflict = scopesConflict(a, b);
104
+ return {
105
+ conflict: conflict !== null,
106
+ detail: conflict,
107
+ };
108
+ }
109
+ export async function checkDriftTool(input = {}, serverRepoRoot) {
110
+ const repoRoot = requireRepoRoot(serverRepoRoot);
111
+ const paths = scopelockPaths(repoRoot);
112
+ const config = await loadConfig(paths);
113
+ const activeId = await getActiveContractId(paths);
114
+ if (activeId === null) {
115
+ throw new Error("no active approved contract; approve one with `scopelock contract approve <file>`");
116
+ }
117
+ const contract = await loadContract(paths, activeId);
118
+ const approvalIntegrity = await verifyApprovalSeal(repoRoot, contract);
119
+ if (!approvalIntegrity.ok) {
120
+ throw new Error(`approval integrity failed: ${approvalIntegrity.detail}`);
121
+ }
122
+ const baselineSha = input.base ?? contract.baseline?.headSha ?? null;
123
+ if (baselineSha === null) {
124
+ throw new Error("active contract has no baseline; approve it with `scopelock contract approve <file>`");
125
+ }
126
+ if (!commitExists(repoRoot, baselineSha)) {
127
+ throw new Error(`baseline commit ${baselineSha} not found (history rewritten?); run \`scopelock contract rebaseline\``);
128
+ }
129
+ const collected = await collectChangedFiles(repoRoot, baselineSha, {
130
+ degradedThreshold: config.degradedFileThreshold,
131
+ });
132
+ const checkedAt = new Date().toISOString();
133
+ const report = buildDriftReport({
134
+ contract,
135
+ files: collected.files,
136
+ repoState: collected.repoState,
137
+ repoMode: collected.repoMode,
138
+ projectTypes: config.projectTypes,
139
+ checkedAt,
140
+ });
141
+ const reportPath = join(paths.reportsDir, driftReportFileName(checkedAt));
142
+ await writeJsonAtomic(reportPath, report);
143
+ return {
144
+ ok: report.violations.length === 0,
145
+ reportPath,
146
+ report,
147
+ };
148
+ }
149
+ export function createScopeLockMcpServer(repoRoot = requireRepoRoot()) {
150
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
151
+ server.registerTool("plan_parallel", {
152
+ title: "Plan Parallel",
153
+ description: "Build a deterministic ScopeLock wave schedule from a plan JSON object and draft/approved contract files.",
154
+ inputSchema: planParallelInputSchema,
155
+ }, async (input) => jsonContent(await planParallelTool(input, repoRoot)));
156
+ server.registerTool("scopes_conflict", {
157
+ title: "Scopes Conflict",
158
+ description: "Check whether two ScopeLock task scopes conflict and return the concrete witness.",
159
+ inputSchema: scopesConflictInputSchema,
160
+ }, async (input) => jsonContent(scopesConflictTool(input)));
161
+ server.registerTool("check_drift", {
162
+ title: "Check Drift",
163
+ description: "Run ScopeLock drift verification for the active approved contract in a git repository.",
164
+ inputSchema: checkDriftInputSchema,
165
+ }, async (input) => jsonContent(await checkDriftTool(input, repoRoot)));
166
+ return server;
167
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@scopelock/mcp",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "MCP stdio server for ScopeLock's narrow planning and drift-verification tools.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "bin": {
11
+ "scopelock-mcp": "./dist/index.js"
12
+ },
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist/index.d.ts",
23
+ "dist/index.js",
24
+ "dist/tools.d.ts",
25
+ "dist/tools.js"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/Daewooox/ScopeLock.git",
30
+ "directory": "packages/mcp"
31
+ },
32
+ "homepage": "https://github.com/Daewooox/ScopeLock#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/Daewooox/ScopeLock/issues"
35
+ },
36
+ "keywords": [
37
+ "mcp",
38
+ "model-context-protocol",
39
+ "ai-coding-agents",
40
+ "scope-control",
41
+ "developer-tools"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "tag": "beta"
46
+ },
47
+ "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.29.0",
49
+ "zod": "^4.4.3",
50
+ "@scopelock/core": "^0.1.0-beta.1"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.json",
54
+ "test": "pnpm run build && node --test dist/*.test.js",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit"
56
+ }
57
+ }