@mhrj/mcp-agent-loop 1.0.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/SKILL.md ADDED
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: Agentic Loop Memory Server
3
+ description: A powerful persistent memory and state-management system serving to allow any AI to execute autonomous agent loops, self-heal, and prevent context window exhaustion.
4
+ ---
5
+
6
+ # Agentic Loop Memory Server Skill ♾️
7
+
8
+ This skill connects you to the \`@mhrj/mcp-agent-loop\` server. This server provides you with persistent memory, preventing you from forgetting your overarching goal or crashing due to context window limits when working on complex, multi-step tasks.
9
+
10
+ ## How it Works
11
+
12
+ When you are asked to perform a complex, autonomous, or long-running task, you must rely on this MCP server instead of trying to hold the entire history in your immediate memory window.
13
+
14
+ **1. Start the Session**
15
+ Run the \`init_loop\` tool with a unique \`session_id\` (e.g. \`debug_auth_bug\`) and the \`objective\`.
16
+
17
+ **2. Read the State**
18
+ Read the MCP resource \`loop://{session_id}\`. This Markdown file is your "brain". It contains the Objective, your System Instructions, the Active Context (recent steps), and your Compacted History (what you completed hours/days ago).
19
+
20
+ **3. Act and Log**
21
+ Perform your normal tasks (running bash commands, editing files, searching). After *every significant action*, you MUST call the \`log_step\` tool:
22
+ - \`session_id\`: Your chosen ID.
23
+ - \`action\`: What you tried to do.
24
+ - \`result\`: The command output or file edit result.
25
+ - \`failed\`: A boolean. \`true\` if the command threw an error or didn't do what you expected. \`false\` if it succeeded.
26
+
27
+ **4. The Self-Healing Requirement (CRITICAL)**
28
+ If you set \`failed: true\` in \`log_step\`, you **MUST** provide a \`self_heal_strategy\`.
29
+ This is because you are not allowed to mindlessly retry the same failing tool. If a grep search fails to find a variable, your \`self_heal_strategy\` might be: *"The variable isn't in \`src\`. I will search in the \`lib\` directory or look for tool suggestions."*
30
+ If you forget the \`self_heal_strategy\`, the \`log_step\` tool will explicitly reject your call and make you try again.
31
+
32
+ **5. The Compaction Requirement (CRITICAL)**
33
+ If you run for a long time, the \`Active Context\` in your state file will grow too large, causing you to crash or hallucinate.
34
+ When \`log_step\` returns a warning that the context is too large (e.g., >3000 words), you **MUST** immediately stop working on the task and call the \`compact_memory\` tool.
35
+ - \`context_summary\`: You must look at the Active Context and write a dense, 2-3 paragraph summary of what was achieved and what the current state is. The server will wipe the Active Context and permanently store your summary.
36
+
37
+ **6. Asking the Human**
38
+ If you hit an absolute dead end (e.g., missing API keys, ambiguous requirements, infinite error loops), do NOT guess. Call the \`report_blocker\` tool. Doing this will pause the loop, allowing you to ask the human user for help via standard chat. Once the human replies, use the \`resume_loop\` tool to inject their input back into the state file.
39
+
40
+ ## Expected Behavior
41
+
42
+ You are expected to act like a senior engineer. Do not give up easily. If an action fails, use your reasoning to devise a new \`self_heal_strategy\`. If you exhaust all local tools, call \`get_tool_suggestions\` to remind yourself how to break out of the box.
package/build/index.js ADDED
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
+ import * as memory from "./memory.js";
6
+ const server = new Server({
7
+ name: "agent-loop-mcp",
8
+ version: "1.0.0",
9
+ }, {
10
+ capabilities: {
11
+ resources: {},
12
+ tools: {},
13
+ },
14
+ });
15
+ // Helper function to approximate word count
16
+ function approximateWordCount(text) {
17
+ return text.split(/\\s+/).length;
18
+ }
19
+ const CONTEXT_WARNING_THRESHOLD = 3000; // Words
20
+ /**
21
+ * Handle listing resources.
22
+ */
23
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
24
+ return {
25
+ resources: [{
26
+ uri: "loop://{session_id}",
27
+ name: "Agent Loop State",
28
+ description: "Reads the current active state and context for the autonomous agent loop.",
29
+ mimeType: "text/markdown",
30
+ }],
31
+ };
32
+ });
33
+ /**
34
+ * Handle reading resources.
35
+ */
36
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
37
+ const uri = request.params.uri;
38
+ if (!uri.startsWith("loop://")) {
39
+ throw new McpError(ErrorCode.InvalidRequest, "Invalid resource URI");
40
+ }
41
+ const sessionId = uri.replace("loop://", "");
42
+ if (!sessionId) {
43
+ throw new McpError(ErrorCode.InvalidRequest, "Missing session_id in URI");
44
+ }
45
+ const loopMemory = await memory.readMemory(sessionId);
46
+ if (!loopMemory) {
47
+ throw new McpError(ErrorCode.InvalidRequest, `No active loop found for session_id: ${sessionId}`);
48
+ }
49
+ const markdownPath = memory.getSessionFilePath(sessionId);
50
+ const fs = await import('fs/promises');
51
+ const rawContent = await fs.readFile(markdownPath, 'utf-8');
52
+ return {
53
+ contents: [{
54
+ uri,
55
+ mimeType: "text/markdown",
56
+ text: rawContent,
57
+ }],
58
+ };
59
+ });
60
+ /**
61
+ * Handle listing tools.
62
+ */
63
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
64
+ return {
65
+ tools: [
66
+ {
67
+ name: "init_loop",
68
+ description: "Creates a new .md loop state file for the given session to start a continuous, autonomous task.",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ session_id: { type: "string", description: "A unique identifier for this loop session" },
73
+ objective: { type: "string", description: "The overarching objective for the agent to achieve" },
74
+ },
75
+ required: ["session_id", "objective"],
76
+ },
77
+ },
78
+ {
79
+ name: "log_step",
80
+ description: "Appends to the Active Context. Rejects if failed=true but no self_heal_strategy is provided. Warns if context is too large.",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ session_id: { type: "string" },
85
+ action: { type: "string", description: "A short description of what was just done" },
86
+ result: { type: "string", description: "The output, success, or failure message of the action" },
87
+ failed: { type: "boolean", description: "Set to true if this step encountered an error or failed to achieve its micro-goal." },
88
+ self_heal_strategy: { type: "string", description: "MANDATORY if failed=true. How you plan to fix this failure or what alternative tool you will explore next." },
89
+ },
90
+ required: ["session_id", "action", "result", "failed"],
91
+ },
92
+ },
93
+ {
94
+ name: "compact_memory",
95
+ description: "Empties the Active Context and appends the AI-provided summary to the Compacted History.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ session_id: { type: "string" },
100
+ context_summary: { type: "string", description: "A highly condensed summary of the current Active Context to preserve important facts and outcomes." },
101
+ },
102
+ required: ["session_id", "context_summary"],
103
+ },
104
+ },
105
+ {
106
+ name: "report_blocker",
107
+ description: "Updates state to STATUS_BLOCKED and asks for human intervention when absolutely stuck.",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ session_id: { type: "string" },
112
+ reason: { type: "string", description: "The reason the loop is blocked and needs human help" },
113
+ },
114
+ required: ["session_id", "reason"],
115
+ },
116
+ },
117
+ {
118
+ name: "resume_loop",
119
+ description: "Removes the block and adds human input context back into the loop.",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ session_id: { type: "string" },
124
+ user_input: { type: "string", description: "The clarify or credentials provided by the human" },
125
+ },
126
+ required: ["session_id", "user_input"],
127
+ },
128
+ },
129
+ {
130
+ name: "get_tool_suggestions",
131
+ description: "Ask this tool if you are stuck and don't know what other tools to use.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {},
135
+ },
136
+ },
137
+ ],
138
+ };
139
+ });
140
+ /**
141
+ * Handle executing tools.
142
+ */
143
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
144
+ const { name, arguments: args } = request.params;
145
+ if (name === "get_tool_suggestions") {
146
+ return {
147
+ content: [{ type: "text", text: "To find alternative tools, ask the host application (like Claude or the MCP Client) to list all available tools on the network. Or simply think out loud about other standard file/terminal tools you might use. You must explore alternatives instead of retrying exactly the same action." }],
148
+ };
149
+ }
150
+ const { session_id } = args;
151
+ if (!session_id || typeof session_id !== 'string') {
152
+ throw new McpError(ErrorCode.InvalidParams, "session_id is required for this tool.");
153
+ }
154
+ if (name === "init_loop") {
155
+ const { objective } = args;
156
+ let loopMemory = await memory.readMemory(session_id);
157
+ if (loopMemory) {
158
+ return { content: [{ type: "text", text: `Session ${session_id} already exists. Please pick a new session_id or read resource loop://${session_id} to continue.` }] };
159
+ }
160
+ loopMemory = {
161
+ state: {
162
+ session_id,
163
+ status: "IN_PROGRESS",
164
+ current_step: 0,
165
+ },
166
+ objective: objective,
167
+ system_instructions: memory.DEFAULT_INSTRUCTIONS,
168
+ active_context: "Session started.",
169
+ compacted_history: "",
170
+ };
171
+ await memory.writeMemory(session_id, loopMemory);
172
+ return {
173
+ content: [{ type: "text", text: `Successfully initialized loop session ${session_id}. Please read resource loop://${session_id} to view your mission.` }],
174
+ };
175
+ }
176
+ const loopMemory = await memory.readMemory(session_id);
177
+ if (!loopMemory) {
178
+ throw new McpError(ErrorCode.InvalidParams, `Session ${session_id} not found. Use init_loop first.`);
179
+ }
180
+ if (name === "log_step") {
181
+ const { action, result, failed, self_heal_strategy } = args;
182
+ if (loopMemory.state.status === "BLOCKED") {
183
+ return { content: [{ type: "text", text: "Session is BLOCKED. Cannot log steps until resume_loop is called." }] };
184
+ }
185
+ // Strict validation for failed steps
186
+ if (failed === true && (!self_heal_strategy || self_heal_strategy.trim() === '')) {
187
+ return {
188
+ isError: true,
189
+ content: [{ type: "text", text: "ERROR: You marked this step as failed (failed: true) but did not provide a 'self_heal_strategy'. You must think of a strategy to fix this issue or explore an alternative tool, and try logging this step again with the strategy attached." }]
190
+ };
191
+ }
192
+ loopMemory.state.current_step += 1;
193
+ if (self_heal_strategy) {
194
+ loopMemory.state.self_heal_strategy = self_heal_strategy;
195
+ }
196
+ else {
197
+ // Clear previous strategy if we succeeded
198
+ delete loopMemory.state.self_heal_strategy;
199
+ }
200
+ const timestamp = new Date().toISOString();
201
+ loopMemory.active_context += `\n\n---\n**Step ${loopMemory.state.current_step}** [${timestamp}]\n*Action:* ${action}\n*Result:* ${result}`;
202
+ await memory.writeMemory(session_id, loopMemory);
203
+ let responseMessage = `Logged step ${loopMemory.state.current_step}.`;
204
+ const wordCount = approximateWordCount(loopMemory.active_context);
205
+ if (wordCount > CONTEXT_WARNING_THRESHOLD) {
206
+ responseMessage += `\n\nWARNING: Active Context is now very large (${wordCount} words). You MUST call 'compact_memory' on your next turn to prevent window overflow.`;
207
+ }
208
+ else {
209
+ responseMessage += ` (Context size: ${wordCount} words). Read loop://${session_id} if you lost track of the state.`;
210
+ }
211
+ return {
212
+ content: [{ type: "text", text: responseMessage }],
213
+ };
214
+ }
215
+ if (name === "compact_memory") {
216
+ const { context_summary } = args;
217
+ if (loopMemory.state.status === "BLOCKED") {
218
+ return { content: [{ type: "text", text: "Session is BLOCKED. Cannot compact memory until resume_loop is called." }] };
219
+ }
220
+ const timestamp = new Date().toISOString();
221
+ loopMemory.compacted_history += `\n\n**Summary up to Step ${loopMemory.state.current_step}** [${timestamp}]\n${context_summary}`;
222
+ loopMemory.active_context = "Context was compacted. Resuming from summary...";
223
+ await memory.writeMemory(session_id, loopMemory);
224
+ return {
225
+ content: [{ type: "text", text: "Successfully compacted memory. Context window footprint reduced. Read loop://" + session_id + " to perceive the compacted state." }],
226
+ };
227
+ }
228
+ if (name === "report_blocker") {
229
+ const { reason } = args;
230
+ loopMemory.state.status = "BLOCKED";
231
+ loopMemory.active_context += `\n\n***\n**BLOCKER REPORTED:**\n${reason}\n***\n`;
232
+ await memory.writeMemory(session_id, loopMemory);
233
+ return {
234
+ content: [{ type: "text", text: `STATUS_BLOCKED: ${reason}. You must now STOP interacting with tools and explicitly ask the human user for clarification or help using your chat interface.` }],
235
+ };
236
+ }
237
+ if (name === "resume_loop") {
238
+ const { user_input } = args;
239
+ if (loopMemory.state.status !== "BLOCKED") {
240
+ return { content: [{ type: "text", text: "Session is not blocked, so nothing to resume." }] };
241
+ }
242
+ loopMemory.state.status = "IN_PROGRESS";
243
+ loopMemory.active_context += `\n\n***\n**BLOCK RESOLVED (HUMAN INPUT):**\n${user_input}\n***\n`;
244
+ await memory.writeMemory(session_id, loopMemory);
245
+ return {
246
+ content: [{ type: "text", text: "Session resumed. Check loop state and continue your self-healing loop." }],
247
+ };
248
+ }
249
+ throw new McpError(ErrorCode.MethodNotFound, "Tool not found");
250
+ });
251
+ async function main() {
252
+ const transport = new StdioServerTransport();
253
+ await server.connect(transport);
254
+ console.error("Agent Loop MCP server running on stdio");
255
+ }
256
+ main().catch((error) => {
257
+ console.error("Server error:", error);
258
+ process.exit(1);
259
+ });
@@ -0,0 +1,103 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as fsSync from 'fs';
3
+ import * as path from 'path';
4
+ import matter from 'gray-matter';
5
+ import { z } from 'zod';
6
+ import * as lockfile from 'proper-lockfile';
7
+ import * as os from 'os';
8
+ export const AgentStateSchema = z.object({
9
+ status: z.enum(['IN_PROGRESS', 'COMPLETED', 'BLOCKED', 'FAILED']).default('IN_PROGRESS'),
10
+ session_id: z.string(),
11
+ current_step: z.number().default(0),
12
+ self_heal_strategy: z.string().optional(),
13
+ last_updated: z.string().optional(),
14
+ });
15
+ const MEMORY_DIR = path.join(os.homedir(), '.agent-loop-mcp');
16
+ // Ensure memory directory exists
17
+ if (!fsSync.existsSync(MEMORY_DIR)) {
18
+ fsSync.mkdirSync(MEMORY_DIR, { recursive: true });
19
+ }
20
+ export function getSessionFilePath(sessionId) {
21
+ // Sanitize session id to avoid path traversal
22
+ const sanitized = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
23
+ return path.join(MEMORY_DIR, `${sanitized}.md`);
24
+ }
25
+ /**
26
+ * Parses the Markdown file into structured LoopMemory.
27
+ * Uses gray-matter to separate frontmatter and markdown body.
28
+ */
29
+ export async function readMemory(sessionId) {
30
+ const filePath = getSessionFilePath(sessionId);
31
+ try {
32
+ const rawContent = await fs.readFile(filePath, 'utf-8');
33
+ const { data, content } = matter(rawContent);
34
+ // Validate frontmatter
35
+ const state = AgentStateSchema.parse(data);
36
+ // Naive parsing of sections
37
+ const objectiveMatch = content.match(/# Objective\n([\s\S]*?)(?=\n# System Instructions \(Read Only\)|\n# Active Context \(Detailed\)|\n# Compacted History \(Summarized\)|$)/);
38
+ const instructionsMatch = content.match(/# System Instructions \(Read Only\)\n([\s\S]*?)(?=\n# Active Context \(Detailed\)|\n# Compacted History \(Summarized\)|$)/);
39
+ const contextMatch = content.match(/# Active Context \(Detailed\)\n([\s\S]*?)(?=\n# Compacted History \(Summarized\)|$)/);
40
+ const historyMatch = content.match(/# Compacted History \(Summarized\)\n([\s\S]*?)$/);
41
+ return {
42
+ state,
43
+ objective: objectiveMatch ? objectiveMatch[1].trim() : '',
44
+ system_instructions: instructionsMatch ? instructionsMatch[1].trim() : '',
45
+ active_context: contextMatch ? contextMatch[1].trim() : '',
46
+ compacted_history: historyMatch ? historyMatch[1].trim() : ''
47
+ };
48
+ }
49
+ catch (error) {
50
+ if (error.code === 'ENOENT') {
51
+ return null;
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+ /**
57
+ * Writes the structured LoopMemory back to the Markdown file atomically with a file lock.
58
+ */
59
+ export async function writeMemory(sessionId, memory) {
60
+ const filePath = getSessionFilePath(sessionId);
61
+ // Ensure the file exists before locking
62
+ let fileExisted = true;
63
+ if (!fsSync.existsSync(filePath)) {
64
+ await fs.writeFile(filePath, '', 'utf-8');
65
+ fileExisted = false;
66
+ }
67
+ let release;
68
+ try {
69
+ release = await lockfile.lock(filePath, { retries: 5 });
70
+ // Update timestamp
71
+ memory.state.last_updated = new Date().toISOString();
72
+ const markdownContent = `
73
+ # Objective
74
+ ${memory.objective}
75
+
76
+ # System Instructions (Read Only)
77
+ ${memory.system_instructions}
78
+
79
+ # Active Context (Detailed)
80
+ ${memory.active_context}
81
+
82
+ # Compacted History (Summarized)
83
+ ${memory.compacted_history}
84
+ `.trim();
85
+ const fileContent = matter.stringify(markdownContent, memory.state);
86
+ // Write atomically (write to temp then rename)
87
+ const tempPath = `${filePath}.tmp.${Date.now()}`;
88
+ await fs.writeFile(tempPath, fileContent, 'utf-8');
89
+ await fs.rename(tempPath, filePath);
90
+ }
91
+ finally {
92
+ if (release) {
93
+ await release();
94
+ }
95
+ }
96
+ }
97
+ export const DEFAULT_INSTRUCTIONS = `
98
+ You are in a continuous self-healing loop. Your goal is to achieve the Objective.
99
+ 1. Use \`log_step\` to report actions you take and their results.
100
+ 2. If an action fails or you encounter an error, do NOT give up. You MUST analyze the failure, set \`failed: true\` in \`log_step\`, and provide a \`self_heal_strategy\`.
101
+ 3. If the \`active_context\` gets too long, \`log_step\` will warn you. You MUST immediately use \`compact_memory\` to summarize older context and free up space.
102
+ 4. If you hit an absolute dead end that requires human permissions, credentials, or fundamentally ambiguous clarification, use \`report_blocker\`.
103
+ `.trim();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@mhrj/mcp-agent-loop",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "build/index.js",
6
+ "bin": {
7
+ "mcp-agent-loop": "build/index.js"
8
+ },
9
+ "files": [
10
+ "build/",
11
+ "SKILL.md",
12
+ "skill.sh"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node build/index.js",
17
+ "test": "echo \"Error: no test specified\" && exit 1"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/meharajM/agent-loop-mcp.git"
22
+ },
23
+ "keywords": [],
24
+ "author": "",
25
+ "license": "ISC",
26
+ "type": "module",
27
+ "bugs": {
28
+ "url": "https://github.com/meharajM/agent-loop-mcp/issues"
29
+ },
30
+ "homepage": "https://github.com/meharajM/agent-loop-mcp#readme",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.27.1",
33
+ "gray-matter": "^4.0.3",
34
+ "proper-lockfile": "^4.1.2",
35
+ "zod": "^4.3.6"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.5.0",
39
+ "@types/proper-lockfile": "^4.1.4",
40
+ "ts-node": "^10.9.2",
41
+ "typescript": "^5.9.3"
42
+ }
43
+ }
package/skill.sh ADDED
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # skill.sh - Helper to install the Agentic Loop Memory skill to local agent folders
3
+
4
+ SKILL_NAME="SKILL.md"
5
+ TARGET_DIR="$HOME/.agents/skills"
6
+
7
+ if [ ! -f "$SKILL_NAME" ]; then
8
+ # If run from node_modules, try to find it
9
+ SKILL_NAME="$(dirname "$0")/$SKILL_NAME"
10
+ fi
11
+
12
+ if [ ! -f "$SKILL_NAME" ]; then
13
+ echo "Error: $SKILL_NAME not found."
14
+ exit 1
15
+ fi
16
+
17
+ mkdir -p "$TARGET_DIR"
18
+ cp "$SKILL_NAME" "$TARGET_DIR/"
19
+ echo "Successfully installed $SKILL_NAME to $TARGET_DIR"