@oleksandr.rudnychenko/sync_loop 0.2.2 → 0.2.4

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/src/server.js CHANGED
@@ -1,208 +1,289 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { z } from "zod";
4
- import { readFileSync } from "node:fs";
5
- import { join, dirname } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { init } from "./init.js";
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const TEMPLATE_DIR = join(__dirname, "..", "template");
11
-
12
- function readTemplate(relativePath) {
13
- return readFileSync(join(TEMPLATE_DIR, relativePath), "utf-8");
14
- }
15
-
16
- // ---------------------------------------------------------------------------
17
- // Doc registry — each entry maps to a template file
18
- // ---------------------------------------------------------------------------
19
- const DOCS = {
20
- "overview": {
21
- path: ".agent-loop/README.md",
22
- name: "Protocol Overview",
23
- description: "SyncLoop file index and framework overview",
24
- },
25
- "agents-md": {
26
- path: "AGENTS.md",
27
- name: "AGENTS.md Template",
28
- description: "Root entrypoint template for AI agents — project identity, protocol, guardrails",
29
- },
30
- "protocol-summary": {
31
- path: "protocol-summary.md",
32
- name: "Protocol Summary",
33
- description: "Condensed 7-stage reasoning loop overview (~50 lines)",
34
- },
35
- "reasoning-kernel": {
36
- path: ".agent-loop/reasoning-kernel.md",
37
- name: "Reasoning Kernel",
38
- description: "Core 7-stage loop, transition map, context clearage, micro/macro classification",
39
- },
40
- "feedback": {
41
- path: ".agent-loop/feedback.md",
42
- name: "Feedback Loop",
43
- description: "Failure diagnosis, patch protocol, micro-loop, branch pruning, learning persistence",
44
- },
45
- "validate-env": {
46
- path: ".agent-loop/validate-env.md",
47
- name: "Validate Environment (Stage 1)",
48
- description: "NFR gates: type safety, tests, layer integrity, complexity, debug hygiene",
49
- },
50
- "validate-n": {
51
- path: ".agent-loop/validate-n.md",
52
- name: "Validate Neighbors (Stage 2)",
53
- description: "Shape compatibility, boundary integrity, bridge contracts",
54
- },
55
- "patterns": {
56
- path: ".agent-loop/patterns.md",
57
- name: "Pattern Registry",
58
- description: "Pattern routing index, architecture baseline, learned patterns, pruning records",
59
- },
60
- "glossary": {
61
- path: ".agent-loop/glossary.md",
62
- name: "Domain Glossary",
63
- description: "Canonical terminology, naming rules, deprecated aliases",
64
- },
65
- "code-patterns": {
66
- path: ".agent-loop/patterns/code-patterns.md",
67
- name: "Code Patterns (P1–P11)",
68
- description: "Port/adapter, domain modules, tasks, routes, DI, config, types, error handling",
69
- },
70
- "testing-guide": {
71
- path: ".agent-loop/patterns/testing-guide.md",
72
- name: "Testing Guide (R2)",
73
- description: "Test pyramid, fixtures, factories, mocks, parametrized tests, naming",
74
- },
75
- "refactoring-workflow": {
76
- path: ".agent-loop/patterns/refactoring-workflow.md",
77
- name: "Refactoring Workflow (R1)",
78
- description: "4-phase checklist for safe file moves and module restructuring",
79
- },
80
- "api-standards": {
81
- path: ".agent-loop/patterns/api-standards.md",
82
- name: "API Standards (R3)",
83
- description: "Boundary contracts, typed models, error envelopes, versioning strategy",
84
- },
85
- };
86
-
87
- // ---------------------------------------------------------------------------
88
- // Server
89
- // ---------------------------------------------------------------------------
90
- const server = new McpServer({
91
- name: "syncloop",
92
- version: "0.1.0",
93
- });
94
-
95
- // ---------------------------------------------------------------------------
96
- // Resources — each protocol doc exposed as a readable resource
97
- // ---------------------------------------------------------------------------
98
- for (const [id, doc] of Object.entries(DOCS)) {
99
- server.resource(
100
- doc.name,
101
- `syncloop://docs/${id}`,
102
- async (uri) => ({
103
- contents: [{
104
- uri: uri.href,
105
- mimeType: "text/markdown",
106
- text: readTemplate(doc.path),
107
- }],
108
- }),
109
- );
110
- }
111
-
112
- // ---------------------------------------------------------------------------
113
- // Tools
114
- // ---------------------------------------------------------------------------
115
-
116
- const StackSchema = z.object({
117
- name: z.string().describe("Stack/layer name (e.g. 'backend', 'frontend', 'auth-service', 'worker')"),
118
- languages: z.array(z.string()).describe("Programming languages used (e.g. ['TypeScript', 'Python'])"),
119
- frameworks: z.array(z.string()).describe("Frameworks used (e.g. ['Next.js', 'Tailwind'] or ['FastAPI', 'SQLAlchemy'])"),
120
- testRunner: z.string().optional().describe("Test runner command (e.g. 'jest', 'pytest', 'vitest')"),
121
- typeChecker: z.string().optional().describe("Type checker command (e.g. 'tsc --noEmit', 'mypy', 'pyright')"),
122
- linter: z.string().optional().describe("Lint/format command (e.g. 'eslint .', 'ruff check')"),
123
- packageManager: z.string().optional().describe("Package manager (e.g. 'npm', 'pnpm', 'poetry')"),
124
- path: z.string().optional().describe("Root directory of this stack relative to project root (e.g. 'apps/web', 'services/auth'). Omit for monolith."),
125
- });
126
-
127
- server.tool(
128
- "init",
129
- "Scaffold SyncLoop protocol files into a project. Before calling this tool, the agent MUST ask the user to confirm the SyncLoop target platform (`copilot`, `cursor`, `claude`, or `all`) unless the user already explicitly provided it. Then scan the codebase to detect all stacks (frontend, backend, services, etc.) and pass them as the stacks array. After scaffolding, the agent MUST scan again and update the generated files with actual project data.",
130
- {
131
- projectPath: z.string().describe("Absolute path to the project root directory"),
132
- target: z.enum(["copilot", "cursor", "claude", "all"]).describe(
133
- "SyncLoop target platform (must be user-confirmed before tool call): copilot (.github/instructions/), cursor (.cursor/rules/), claude (.claude/rules/ + CLAUDE.md), or all",
134
- ),
135
- stacks: z.array(StackSchema).min(1).describe(
136
- "Project stacks one entry per layer/service. Fullstack app: [{name:'backend',...},{name:'frontend',...}]. Monolith: [{name:'app',...}]. Microservices: one per service.",
137
- ),
138
- },
139
- async ({ projectPath, target, stacks }) => {
140
- try {
141
- const results = init(projectPath, target, stacks);
142
- const bootstrapPrompt = readTemplate("bootstrap-prompt.md");
143
-
144
- const stackSummary = stacks.map(s => [
145
- `\n### ${s.name}${s.path ? ` (${s.path})` : ""}`,
146
- `- Languages: ${s.languages.join(", ")}`,
147
- `- Frameworks: ${s.frameworks.join(", ")}`,
148
- s.testRunner ? `- Test runner: ${s.testRunner}` : null,
149
- s.typeChecker ? `- Type checker: ${s.typeChecker}` : null,
150
- s.linter ? `- Linter: ${s.linter}` : null,
151
- s.packageManager ? `- Package manager: ${s.packageManager}` : null,
152
- ].filter(Boolean).join("\n")).join("\n");
153
-
154
- return {
155
- content: [
156
- { type: "text", text: `SyncLoop initialized for ${target}:\n\n${results.join("\n")}` },
157
- { type: "text", text: `\n---\n\n## Detected stacks\n${stackSummary}` },
158
- { type: "text", text: `\n---\n\n**IMPORTANT: Now scan the codebase and wire the generated SyncLoop files to this project.**\n\n${bootstrapPrompt}` },
159
- ],
160
- };
161
- } catch (err) {
162
- return {
163
- content: [{ type: "text", text: `Error initializing SyncLoop: ${err.message}` }],
164
- isError: true,
165
- };
166
- }
167
- },
168
- );
169
-
170
- // ---------------------------------------------------------------------------
171
- // Prompts
172
- // ---------------------------------------------------------------------------
173
-
174
- server.prompt(
175
- "bootstrap",
176
- "Bootstrap prompt — wire SyncLoop protocol to an existing project by scanning its codebase",
177
- async () => ({
178
- description: "Scan the project codebase and wire SyncLoop protocol references to real project structure",
179
- messages: [{
180
- role: "user",
181
- content: {
182
- type: "text",
183
- text: readTemplate("bootstrap-prompt.md"),
184
- },
185
- }],
186
- }),
187
- );
188
-
189
- server.prompt(
190
- "protocol",
191
- "SyncLoop reasoning protocol summary — inject as system context for any AI coding task",
192
- async () => ({
193
- description: "The SyncLoop 7-stage reasoning protocol for self-correcting agent behavior",
194
- messages: [{
195
- role: "user",
196
- content: {
197
- type: "text",
198
- text: readTemplate("protocol-summary.md"),
199
- },
200
- }],
201
- }),
202
- );
203
-
204
- // ---------------------------------------------------------------------------
205
- // Connect transport
206
- // ---------------------------------------------------------------------------
207
- const transport = new StdioServerTransport();
208
- await server.connect(transport);
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { readFileSync } from "node:fs";
5
+ import { join, dirname, resolve, posix } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { init, detectStacks } from "./init.js";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const TEMPLATE_DIR = join(__dirname, "template");
11
+ const PACKAGE_JSON = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
12
+
13
+ function readTemplate(relativePath) {
14
+ return readFileSync(join(TEMPLATE_DIR, relativePath), "utf-8");
15
+ }
16
+
17
+ function pathDir(value) {
18
+ const dir = posix.dirname(value);
19
+ return dir === "." ? "" : dir;
20
+ }
21
+
22
+ function splitHash(link) {
23
+ const idx = link.indexOf("#");
24
+ if (idx === -1) return { pathPart: link, hash: "" };
25
+ return {
26
+ pathPart: link.slice(0, idx),
27
+ hash: link.slice(idx),
28
+ };
29
+ }
30
+
31
+ function isExternalLink(link) {
32
+ return /^([a-z]+:|#)/i.test(link);
33
+ }
34
+
35
+ function rewriteMarkdownLinks(content, transform) {
36
+ let inFence = false;
37
+ return content
38
+ .split("\n")
39
+ .map((line) => {
40
+ if (line.trimStart().startsWith("```")) {
41
+ inFence = !inFence;
42
+ return line;
43
+ }
44
+ if (inFence) return line;
45
+ return line.replace(/\]\(([^)]+)\)/g, (_match, linkPath) => `](${transform(linkPath)})`);
46
+ })
47
+ .join("\n");
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Doc registry
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const DOCS = {
55
+ "overview": {
56
+ path: ".agent-loop/README.md",
57
+ name: "Protocol Overview",
58
+ description: "SyncLoop file index and framework overview",
59
+ },
60
+ "agents-md": {
61
+ path: "AGENTS.md",
62
+ name: "AGENTS.md Template",
63
+ description: "Root entrypoint template for AI agents - project identity, protocol, guardrails",
64
+ },
65
+ "protocol-summary": {
66
+ path: "protocol-summary.md",
67
+ name: "Protocol Summary",
68
+ description: "Condensed 7-stage reasoning loop overview (~50 lines)",
69
+ },
70
+ "reasoning-kernel": {
71
+ path: ".agent-loop/reasoning-kernel.md",
72
+ name: "Reasoning Kernel",
73
+ description: "Core 7-stage loop, transition map, context clearage, micro/macro classification",
74
+ },
75
+ "feedback": {
76
+ path: ".agent-loop/feedback.md",
77
+ name: "Feedback Loop",
78
+ description: "Failure diagnosis, patch protocol, micro-loop, branch pruning, learning persistence",
79
+ },
80
+ "validate-env": {
81
+ path: ".agent-loop/validate-env.md",
82
+ name: "Validate Environment (Stage 1)",
83
+ description: "NFR gates: type safety, tests, layer integrity, complexity, debug hygiene",
84
+ },
85
+ "validate-n": {
86
+ path: ".agent-loop/validate-n.md",
87
+ name: "Validate Neighbors (Stage 2)",
88
+ description: "Shape compatibility, boundary integrity, bridge contracts",
89
+ },
90
+ "patterns": {
91
+ path: ".agent-loop/patterns.md",
92
+ name: "Pattern Registry",
93
+ description: "Pattern routing index, architecture baseline, learned patterns, pruning records",
94
+ },
95
+ "glossary": {
96
+ path: ".agent-loop/glossary.md",
97
+ name: "Domain Glossary",
98
+ description: "Canonical terminology, naming rules, deprecated aliases",
99
+ },
100
+ "code-patterns": {
101
+ path: ".agent-loop/patterns/code-patterns.md",
102
+ name: "Code Patterns (P1-P11)",
103
+ description: "Port/adapter, domain modules, tasks, routes, DI, config, types, error handling",
104
+ },
105
+ "testing-guide": {
106
+ path: ".agent-loop/patterns/testing-guide.md",
107
+ name: "Testing Guide (R2)",
108
+ description: "Test pyramid, fixtures, factories, mocks, parametrized tests, naming",
109
+ },
110
+ "refactoring-workflow": {
111
+ path: ".agent-loop/patterns/refactoring-workflow.md",
112
+ name: "Refactoring Workflow (R1)",
113
+ description: "4-phase checklist for safe file moves and module restructuring",
114
+ },
115
+ "api-standards": {
116
+ path: ".agent-loop/patterns/api-standards.md",
117
+ name: "API Standards (R3)",
118
+ description: "Boundary contracts, typed models, error envelopes, versioning strategy",
119
+ },
120
+ };
121
+
122
+ const DOC_ID_BY_PATH = {};
123
+ for (const [id, doc] of Object.entries(DOCS)) {
124
+ DOC_ID_BY_PATH[doc.path] = id;
125
+ }
126
+
127
+ function rewriteResourceLinks(content, sourcePath) {
128
+ return rewriteMarkdownLinks(content, (linkPath) => {
129
+ if (isExternalLink(linkPath)) return linkPath;
130
+
131
+ const { pathPart, hash } = splitHash(linkPath);
132
+ if (!pathPart) return linkPath;
133
+
134
+ let canonical = posix.normalize(posix.join(pathDir(sourcePath) || ".", pathPart)).replace(/^\.\//, "");
135
+ if (canonical === ".agent-loop/patterns") canonical = ".agent-loop/patterns.md";
136
+ if (canonical === "../AGENTS.md" || canonical === "AGENTS.md") canonical = "AGENTS.md";
137
+ if (canonical === "../README.md") return linkPath;
138
+
139
+ const docId = DOC_ID_BY_PATH[canonical];
140
+ if (!docId) return linkPath;
141
+
142
+ return `syncloop://docs/${docId}${hash}`;
143
+ });
144
+ }
145
+
146
+ function formatStacks(stacks) {
147
+ return stacks
148
+ .map((stack) => [
149
+ `\n### ${stack.name}${stack.path ? ` (${stack.path})` : ""}`,
150
+ `- Languages: ${stack.languages.join(", ")}`,
151
+ `- Frameworks: ${stack.frameworks.join(", ")}`,
152
+ stack.testRunner ? `- Test runner: ${stack.testRunner}` : null,
153
+ stack.typeChecker ? `- Type checker: ${stack.typeChecker}` : null,
154
+ stack.linter ? `- Linter: ${stack.linter}` : null,
155
+ stack.packageManager ? `- Package manager: ${stack.packageManager}` : null,
156
+ ].filter(Boolean).join("\n"))
157
+ .join("\n");
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Server
162
+ // ---------------------------------------------------------------------------
163
+
164
+ const server = new McpServer({
165
+ name: "sync_loop",
166
+ version: PACKAGE_JSON.version,
167
+ });
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Resources
171
+ // ---------------------------------------------------------------------------
172
+
173
+ for (const [id, doc] of Object.entries(DOCS)) {
174
+ server.registerResource(
175
+ doc.name,
176
+ `syncloop://docs/${id}`,
177
+ async (uri) => {
178
+ const raw = readTemplate(doc.path);
179
+ const rewritten = rewriteResourceLinks(raw, doc.path);
180
+ return {
181
+ contents: [{
182
+ uri: uri.href,
183
+ mimeType: "text/markdown",
184
+ text: rewritten,
185
+ }],
186
+ };
187
+ },
188
+ );
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Tools
193
+ // ---------------------------------------------------------------------------
194
+
195
+ const StackSchema = z.object({
196
+ name: z.string().describe("Stack/layer name (for example: backend, frontend, worker)"),
197
+ languages: z.array(z.string()).describe("Programming languages used by this stack"),
198
+ frameworks: z.array(z.string()).describe("Frameworks/libraries for this stack"),
199
+ testRunner: z.string().optional().describe("Test runner command"),
200
+ typeChecker: z.string().optional().describe("Type checker command"),
201
+ linter: z.string().optional().describe("Lint/format command"),
202
+ packageManager: z.string().optional().describe("Package manager name"),
203
+ path: z.string().optional().describe("Root directory of this stack relative to project root"),
204
+ });
205
+
206
+ server.registerTool(
207
+ "init",
208
+ "Scaffold SyncLoop protocol files into a project. If target is not explicitly provided, default to all. If stacks are not provided, auto-detect them by scanning the repository.",
209
+ {
210
+ projectPath: z.string().optional().describe("Project root path. Defaults to current working directory."),
211
+ target: z.enum(["copilot", "cursor", "claude", "all"]).optional().default("all"),
212
+ stacks: z.array(StackSchema).optional().describe("Optional stack definitions. Auto-detected when omitted."),
213
+ dryRun: z.boolean().optional().default(false).describe("Preview file writes without modifying files."),
214
+ overwrite: z.boolean().optional().default(true).describe("Overwrite existing generated files."),
215
+ },
216
+ async ({ projectPath, target = "all", stacks, dryRun = false, overwrite = true }) => {
217
+ try {
218
+ const resolvedProjectPath = resolve(projectPath ?? process.cwd());
219
+ const effectiveStacks = stacks?.length ? stacks : detectStacks(resolvedProjectPath);
220
+ const initResult = init(
221
+ resolvedProjectPath,
222
+ target,
223
+ effectiveStacks,
224
+ { dryRun, overwrite },
225
+ );
226
+ const bootstrapPrompt = readTemplate("bootstrap-prompt.md");
227
+ const stackSummary = formatStacks(initResult.stacks);
228
+
229
+ return {
230
+ content: [
231
+ { type: "text", text: `SyncLoop initialized for ${target}:\n\n${initResult.results.join("\n")}` },
232
+ { type: "text", text: `\n---\n\n## Options\n- dryRun: ${dryRun}\n- overwrite: ${overwrite}` },
233
+ { type: "text", text: `\n---\n\n## Detected stacks\n${stackSummary}` },
234
+ {
235
+ type: "text",
236
+ text: `\n---\n\n**IMPORTANT: Now scan the codebase and wire the generated SyncLoop files to this project.**\n\n${bootstrapPrompt}`,
237
+ },
238
+ { type: "text", text: `\n---\n\n## Machine-readable result\n\`\`\`json\n${JSON.stringify(initResult, null, 2)}\n\`\`\`` },
239
+ ],
240
+ };
241
+ } catch (err) {
242
+ return {
243
+ content: [{ type: "text", text: `Error initializing SyncLoop: ${err.message}` }],
244
+ isError: true,
245
+ };
246
+ }
247
+ },
248
+ );
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Prompts
252
+ // ---------------------------------------------------------------------------
253
+
254
+ server.registerPrompt(
255
+ "bootstrap",
256
+ "Bootstrap prompt - wire SyncLoop protocol to an existing project by scanning its codebase",
257
+ async () => ({
258
+ description: "Scan the project codebase and wire SyncLoop protocol references to real project structure",
259
+ messages: [{
260
+ role: "user",
261
+ content: {
262
+ type: "text",
263
+ text: readTemplate("bootstrap-prompt.md"),
264
+ },
265
+ }],
266
+ }),
267
+ );
268
+
269
+ server.registerPrompt(
270
+ "protocol",
271
+ "SyncLoop reasoning protocol summary - inject as system context for any AI coding task",
272
+ async () => ({
273
+ description: "The SyncLoop 7-stage reasoning protocol for self-correcting agent behavior",
274
+ messages: [{
275
+ role: "user",
276
+ content: {
277
+ type: "text",
278
+ text: readTemplate("protocol-summary.md"),
279
+ },
280
+ }],
281
+ }),
282
+ );
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Connect transport
286
+ // ---------------------------------------------------------------------------
287
+
288
+ const transport = new StdioServerTransport();
289
+ await server.connect(transport);
@@ -11,9 +11,9 @@ Add the SyncLoop MCP server to your AI coding client. The agent gets the full pr
11
11
  ```json
12
12
  {
13
13
  "mcpServers": {
14
- "syncloop": {
14
+ "sync_loop": {
15
15
  "command": "npx",
16
- "args": ["-y", "syncloop"]
16
+ "args": ["-y", "-p", "@oleksandr.rudnychenko/sync_loop", "sync_loop"]
17
17
  }
18
18
  }
19
19
  }
@@ -38,7 +38,7 @@ If you want the protocol files in your project (for offline use, customization,
38
38
 
39
39
  ### Via MCP (ask the agent)
40
40
 
41
- Ask: "Use the syncloop init tool to scaffold files for [copilot/cursor/claude/all] into this project"
41
+ Ask: "Use the sync_loop init tool to scaffold files for [copilot/cursor/claude/all] into this project"
42
42
 
43
43
  Before calling `init`, the agent should ask:
44
44
  "Which SyncLoop target platform should I scaffold: `copilot`, `cursor`, `claude`, or `all`?"
File without changes
File without changes
File without changes