nodus-mcp-server 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shawn Knight
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.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: nodus-mcp-server
3
+ Version: 0.1.0
4
+ Summary: MCP server powered by the Nodus orchestration runtime
5
+ Author-email: Shawn Knight <shawnknight@the-master-plan.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Shawn Knight
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/Masterplanner25/nodus-mcp-server
29
+ Project-URL: Repository, https://github.com/Masterplanner25/nodus-mcp-server
30
+ Project-URL: Issues, https://github.com/Masterplanner25/nodus-mcp-server/issues
31
+ Keywords: mcp,nodus,orchestration,agents,llm,claude
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
37
+ Requires-Python: >=3.10
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: nodus-lang<5.0.0,>=4.0.4
41
+ Requires-Dist: nodus-mcp>=0.1.0
42
+ Dynamic: license-file
43
+
44
+ # nodus-mcp-server
45
+
46
+ An MCP (Model Context Protocol) server that exposes the [Nodus](https://github.com/Masterplanner25/Nodus) orchestration runtime as tools for Claude Desktop and other MCP-compatible hosts.
47
+
48
+ ## What it does
49
+
50
+ Six tools over a single server process:
51
+
52
+ | Tool | Description |
53
+ |------|-------------|
54
+ | `nodus.remember` | Store a fact in persistent SQLite memory with optional tags |
55
+ | `nodus.recall` | Search memory by free-text query and/or tags |
56
+ | `nodus.forget` | Delete a memory entry by ID |
57
+ | `nodus.run_goal` | Run a Nodus goal (sandboxed, structured step results) |
58
+ | `nodus.run_workflow` | Run a Nodus workflow (checkpoint/resume capable) |
59
+ | `nodus.exec` | Execute arbitrary `.nd` code (10 s timeout, no file I/O) |
60
+
61
+ ## Requirements
62
+
63
+ - Python ≥ 3.10
64
+ - `nodus-lang >= 4.0.4`
65
+ - `nodus-mcp >= 0.1.0`
66
+
67
+ ## Install
68
+
69
+ ```
70
+ pip install nodus-lang nodus-mcp nodus-mcp-server
71
+ ```
72
+
73
+ ## Claude Desktop setup
74
+
75
+ Add to your `claude_desktop_config.json`:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "nodus": {
81
+ "command": "nodus-mcp-server",
82
+ "args": ["--stdio"]
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ Restart Claude Desktop. The six `nodus.*` tools will appear in the tool list.
89
+
90
+ Memory persists at `~/.nodus-mcp-server/data/memory.db` and survives upgrades.
91
+
92
+ ## HTTP mode
93
+
94
+ For remote or multi-client use:
95
+
96
+ ```
97
+ nodus-mcp-server --http --port 8080
98
+ nodus-mcp-server --http --port 8080 --bearer-token <secret>
99
+ ```
100
+
101
+ ## Built-in goals and workflows
102
+
103
+ ### Goals
104
+
105
+ **`summarize`** — `params: {text: string}`
106
+
107
+ Counts characters and classifies text size (short / medium / long).
108
+
109
+ ```json
110
+ {
111
+ "name": "summarize",
112
+ "params": {"text": "Your text here"}
113
+ }
114
+ ```
115
+
116
+ Returns:
117
+ ```json
118
+ {
119
+ "steps": {
120
+ "measure": 14,
121
+ "classify": {"chars": 14, "size": "short", "empty": false}
122
+ }
123
+ }
124
+ ```
125
+
126
+ **`pipeline`** — `params: {items: list, label: string}`
127
+
128
+ Validates an item list and produces a labelled report.
129
+
130
+ ```json
131
+ {
132
+ "name": "pipeline",
133
+ "params": {"items": [1, 2, 3], "label": "batch-1"}
134
+ }
135
+ ```
136
+
137
+ Returns:
138
+ ```json
139
+ {
140
+ "steps": {
141
+ "validate": 3,
142
+ "report": {"label": "batch-1", "item_count": 3, "has_items": true, "status": "complete"}
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### Workflows
148
+
149
+ **`research`** — `params: {topic: string}`
150
+
151
+ Two-step planning + execution workflow with checkpoints at each step.
152
+
153
+ ```json
154
+ {
155
+ "name": "research",
156
+ "params": {"topic": "LLM context windows"}
157
+ }
158
+ ```
159
+
160
+ Returns:
161
+ ```json
162
+ {
163
+ "steps": {
164
+ "plan": {"query": "LLM context windows", "strategy": "step-by-step"},
165
+ "execute": {"topic": "LLM context windows", "query": "LLM context windows", "strategy": "step-by-step", "status": "complete"}
166
+ }
167
+ }
168
+ ```
169
+
170
+ ## Adding your own goals and workflows
171
+
172
+ Drop a `.nd` file into `goals/` or `workflows/`. The file should **only define** the goal or workflow — do not call `run_goal()` or `run_workflow()` at the bottom (the server calls it for you).
173
+
174
+ ```nodus
175
+ // goals/my_goal.nd — input variable injected via params
176
+ goal my_goal {
177
+ step process {
178
+ let result = len(input_text)
179
+ return {"length": result, "has_content": result > 0i}
180
+ }
181
+ }
182
+ ```
183
+
184
+ Then call it:
185
+ ```json
186
+ {"name": "my_goal", "params": {"input_text": "hello"}}
187
+ ```
188
+
189
+ ## Sandbox
190
+
191
+ `.nd` scripts run with:
192
+ - No file system access (`allowed_paths=[]`)
193
+ - No network access
194
+ - Goal timeout: 30 s
195
+ - Workflow timeout: 60 s
196
+ - `nodus.exec` timeout: 10 s
197
+
198
+ ## Architecture
199
+
200
+ ```
201
+ server.py — NodusRuntime, tool registration, MCP transport
202
+ runner.py — goal/workflow execution via ModuleLoader + VM
203
+ memory_store.py — SQLite-backed thread-safe memory store
204
+ goals/ — .nd goal definitions
205
+ workflows/ — .nd workflow definitions
206
+ ~/.nodus-mcp-server/data/memory.db — SQLite DB (persists across upgrades)
207
+ ```
208
+
209
+ ## License
210
+
211
+ MIT
@@ -0,0 +1,168 @@
1
+ # nodus-mcp-server
2
+
3
+ An MCP (Model Context Protocol) server that exposes the [Nodus](https://github.com/Masterplanner25/Nodus) orchestration runtime as tools for Claude Desktop and other MCP-compatible hosts.
4
+
5
+ ## What it does
6
+
7
+ Six tools over a single server process:
8
+
9
+ | Tool | Description |
10
+ |------|-------------|
11
+ | `nodus.remember` | Store a fact in persistent SQLite memory with optional tags |
12
+ | `nodus.recall` | Search memory by free-text query and/or tags |
13
+ | `nodus.forget` | Delete a memory entry by ID |
14
+ | `nodus.run_goal` | Run a Nodus goal (sandboxed, structured step results) |
15
+ | `nodus.run_workflow` | Run a Nodus workflow (checkpoint/resume capable) |
16
+ | `nodus.exec` | Execute arbitrary `.nd` code (10 s timeout, no file I/O) |
17
+
18
+ ## Requirements
19
+
20
+ - Python ≥ 3.10
21
+ - `nodus-lang >= 4.0.4`
22
+ - `nodus-mcp >= 0.1.0`
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pip install nodus-lang nodus-mcp nodus-mcp-server
28
+ ```
29
+
30
+ ## Claude Desktop setup
31
+
32
+ Add to your `claude_desktop_config.json`:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "nodus": {
38
+ "command": "nodus-mcp-server",
39
+ "args": ["--stdio"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ Restart Claude Desktop. The six `nodus.*` tools will appear in the tool list.
46
+
47
+ Memory persists at `~/.nodus-mcp-server/data/memory.db` and survives upgrades.
48
+
49
+ ## HTTP mode
50
+
51
+ For remote or multi-client use:
52
+
53
+ ```
54
+ nodus-mcp-server --http --port 8080
55
+ nodus-mcp-server --http --port 8080 --bearer-token <secret>
56
+ ```
57
+
58
+ ## Built-in goals and workflows
59
+
60
+ ### Goals
61
+
62
+ **`summarize`** — `params: {text: string}`
63
+
64
+ Counts characters and classifies text size (short / medium / long).
65
+
66
+ ```json
67
+ {
68
+ "name": "summarize",
69
+ "params": {"text": "Your text here"}
70
+ }
71
+ ```
72
+
73
+ Returns:
74
+ ```json
75
+ {
76
+ "steps": {
77
+ "measure": 14,
78
+ "classify": {"chars": 14, "size": "short", "empty": false}
79
+ }
80
+ }
81
+ ```
82
+
83
+ **`pipeline`** — `params: {items: list, label: string}`
84
+
85
+ Validates an item list and produces a labelled report.
86
+
87
+ ```json
88
+ {
89
+ "name": "pipeline",
90
+ "params": {"items": [1, 2, 3], "label": "batch-1"}
91
+ }
92
+ ```
93
+
94
+ Returns:
95
+ ```json
96
+ {
97
+ "steps": {
98
+ "validate": 3,
99
+ "report": {"label": "batch-1", "item_count": 3, "has_items": true, "status": "complete"}
100
+ }
101
+ }
102
+ ```
103
+
104
+ ### Workflows
105
+
106
+ **`research`** — `params: {topic: string}`
107
+
108
+ Two-step planning + execution workflow with checkpoints at each step.
109
+
110
+ ```json
111
+ {
112
+ "name": "research",
113
+ "params": {"topic": "LLM context windows"}
114
+ }
115
+ ```
116
+
117
+ Returns:
118
+ ```json
119
+ {
120
+ "steps": {
121
+ "plan": {"query": "LLM context windows", "strategy": "step-by-step"},
122
+ "execute": {"topic": "LLM context windows", "query": "LLM context windows", "strategy": "step-by-step", "status": "complete"}
123
+ }
124
+ }
125
+ ```
126
+
127
+ ## Adding your own goals and workflows
128
+
129
+ Drop a `.nd` file into `goals/` or `workflows/`. The file should **only define** the goal or workflow — do not call `run_goal()` or `run_workflow()` at the bottom (the server calls it for you).
130
+
131
+ ```nodus
132
+ // goals/my_goal.nd — input variable injected via params
133
+ goal my_goal {
134
+ step process {
135
+ let result = len(input_text)
136
+ return {"length": result, "has_content": result > 0i}
137
+ }
138
+ }
139
+ ```
140
+
141
+ Then call it:
142
+ ```json
143
+ {"name": "my_goal", "params": {"input_text": "hello"}}
144
+ ```
145
+
146
+ ## Sandbox
147
+
148
+ `.nd` scripts run with:
149
+ - No file system access (`allowed_paths=[]`)
150
+ - No network access
151
+ - Goal timeout: 30 s
152
+ - Workflow timeout: 60 s
153
+ - `nodus.exec` timeout: 10 s
154
+
155
+ ## Architecture
156
+
157
+ ```
158
+ server.py — NodusRuntime, tool registration, MCP transport
159
+ runner.py — goal/workflow execution via ModuleLoader + VM
160
+ memory_store.py — SQLite-backed thread-safe memory store
161
+ goals/ — .nd goal definitions
162
+ workflows/ — .nd workflow definitions
163
+ ~/.nodus-mcp-server/data/memory.db — SQLite DB (persists across upgrades)
164
+ ```
165
+
166
+ ## License
167
+
168
+ MIT
File without changes
@@ -0,0 +1,20 @@
1
+ // Pipeline goal — input: items (list), label (string)
2
+ // Validates input, then produces a report. Steps run in dependency order;
3
+ // validate is a prerequisite for both report branches.
4
+
5
+ goal pipeline {
6
+ step validate {
7
+ let count = len(items)
8
+ checkpoint "validated"
9
+ return count
10
+ }
11
+
12
+ step report after validate {
13
+ return {
14
+ "label": label,
15
+ "item_count": validate,
16
+ "has_items": validate > 0i,
17
+ "status": "complete"
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,17 @@
1
+ // Summarize goal — input: text (string)
2
+ // Measures character count then classifies the text by size.
3
+
4
+ goal summarize {
5
+ step measure {
6
+ let char_count = len(text)
7
+ checkpoint "measured"
8
+ return char_count
9
+ }
10
+
11
+ step classify after measure {
12
+ let size = "short"
13
+ if (measure > 500i) { size = "medium" }
14
+ if (measure > 2000i) { size = "long" }
15
+ return {"chars": measure, "size": size, "empty": measure == 0i}
16
+ }
17
+ }
@@ -0,0 +1,75 @@
1
+ """SQLite-backed persistent memory store."""
2
+ from __future__ import annotations
3
+
4
+ import sqlite3
5
+ import threading
6
+ import time
7
+ import uuid
8
+
9
+
10
+ class MemoryStore:
11
+ def __init__(self, db_path: str) -> None:
12
+ import os
13
+ os.makedirs(os.path.dirname(db_path), exist_ok=True)
14
+ self._path = db_path
15
+ self._lock = threading.Lock()
16
+ self._init_db()
17
+
18
+ def _conn(self) -> sqlite3.Connection:
19
+ conn = sqlite3.connect(self._path, check_same_thread=False)
20
+ conn.row_factory = sqlite3.Row
21
+ return conn
22
+
23
+ def _init_db(self) -> None:
24
+ with self._lock, self._conn() as conn:
25
+ conn.execute("""
26
+ CREATE TABLE IF NOT EXISTS memories (
27
+ id TEXT PRIMARY KEY,
28
+ content TEXT NOT NULL,
29
+ tags TEXT NOT NULL DEFAULT '',
30
+ created_at REAL NOT NULL
31
+ )
32
+ """)
33
+
34
+ def remember(self, content: str, tags: list[str]) -> dict:
35
+ mem_id = str(uuid.uuid4())[:8]
36
+ tags_str = ",".join(t.strip() for t in tags if t.strip())
37
+ with self._lock, self._conn() as conn:
38
+ conn.execute(
39
+ "INSERT INTO memories (id, content, tags, created_at) VALUES (?, ?, ?, ?)",
40
+ (mem_id, content, tags_str, time.time()),
41
+ )
42
+ return {"id": mem_id, "stored": True}
43
+
44
+ def recall(self, query: str, tags: list[str], limit: int) -> list[dict]:
45
+ with self._lock, self._conn() as conn:
46
+ rows = conn.execute(
47
+ "SELECT id, content, tags, created_at FROM memories ORDER BY created_at DESC LIMIT 200"
48
+ ).fetchall()
49
+
50
+ results = []
51
+ query_lower = query.lower()
52
+ filter_tags = {t.strip().lower() for t in tags if t.strip()}
53
+
54
+ for row in rows:
55
+ content = row["content"]
56
+ row_tags = {t for t in row["tags"].split(",") if t}
57
+ if query_lower and query_lower not in content.lower():
58
+ continue
59
+ if filter_tags and not filter_tags.intersection(row_tags):
60
+ continue
61
+ results.append({
62
+ "id": row["id"],
63
+ "content": content,
64
+ "tags": list(row_tags),
65
+ "created_at": row["created_at"],
66
+ })
67
+ if len(results) >= limit:
68
+ break
69
+
70
+ return results
71
+
72
+ def forget(self, memory_id: str) -> dict:
73
+ with self._lock, self._conn() as conn:
74
+ cursor = conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
75
+ return {"id": memory_id, "deleted": cursor.rowcount > 0}
@@ -0,0 +1,100 @@
1
+ """Goal and workflow execution helpers."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Any
6
+
7
+ from nodus.vm.vm import VM
8
+ from nodus.runtime.module_loader import ModuleLoader
9
+ from nodus.tooling.sandbox import capture_output, configure_vm_limits
10
+ from nodus.tooling.runner import _resolve_goal_from_vm, _resolve_workflow_from_vm
11
+ from nodus.support.config import MAX_STEPS, MAX_STDOUT_CHARS
12
+
13
+ _HERE = os.path.dirname(os.path.abspath(__file__))
14
+ GOALS_DIR = os.path.join(_HERE, "goals")
15
+ WORKFLOWS_DIR = os.path.join(_HERE, "workflows")
16
+
17
+
18
+ def _load(directory: str, name: str, kind: str) -> tuple[str, str]:
19
+ safe = os.path.basename(name) # prevent path traversal
20
+ path = os.path.join(directory, f"{safe}.nd")
21
+ if not os.path.isfile(path):
22
+ raise FileNotFoundError(f"{kind} '{safe}' not found (looked in {directory})")
23
+ with open(path, encoding="utf-8") as fh:
24
+ return fh.read(), path
25
+
26
+
27
+ def _load_into_vm(code: str, path: str, params: dict, timeout_ms: int) -> tuple[VM, str]:
28
+ """Compile and execute module-level code (goal/workflow definition) into a sandboxed VM.
29
+
30
+ host_globals must be passed to ModuleLoader — passing them only to the VM constructor
31
+ is not enough because _execute_module overwrites vm.host_globals via reset_program.
32
+ """
33
+ vm = VM([], {}, code_locs=[], source_path=path, allowed_paths=[])
34
+ configure_vm_limits(vm, max_steps=MAX_STEPS, timeout_ms=timeout_ms)
35
+ loader = ModuleLoader(vm=vm, host_globals=params)
36
+ module_name = os.path.abspath(path)
37
+ base_dir = os.path.dirname(module_name)
38
+ with capture_output(max_stdout_chars=MAX_STDOUT_CHARS) as (stdout, _):
39
+ loader.load_module_from_source(code, module_name=module_name, base_dir=base_dir)
40
+ return vm, stdout.getvalue()
41
+
42
+
43
+ def run_goal(runtime: Any, name: str, params: dict) -> dict:
44
+ try:
45
+ code, path = _load(GOALS_DIR, name, "goal")
46
+ except FileNotFoundError as exc:
47
+ return {"ok": False, "error": str(exc)}
48
+ try:
49
+ vm, def_stdout = _load_into_vm(code, path, params, timeout_ms=30_000)
50
+ except Exception as exc:
51
+ return {"ok": False, "error": str(exc)}
52
+ try:
53
+ goal = _resolve_goal_from_vm(vm, name)
54
+ with capture_output(max_stdout_chars=MAX_STDOUT_CHARS) as (run_out, _):
55
+ goal_result = vm.builtin_run_goal(goal)
56
+ except Exception as exc:
57
+ return {"ok": False, "error": str(exc)}
58
+ combined_stdout = (def_stdout + run_out.getvalue()).strip()
59
+ goal_data = goal_result if isinstance(goal_result, dict) else {}
60
+ steps = goal_data.get("steps", {})
61
+ out: dict = {"ok": True, "goal": name, "steps": steps}
62
+ if combined_stdout:
63
+ out["stdout"] = combined_stdout
64
+ return out
65
+
66
+
67
+ def run_workflow(runtime: Any, name: str, params: dict) -> dict:
68
+ try:
69
+ code, path = _load(WORKFLOWS_DIR, name, "workflow")
70
+ except FileNotFoundError as exc:
71
+ return {"ok": False, "error": str(exc)}
72
+ try:
73
+ vm, def_stdout = _load_into_vm(code, path, params, timeout_ms=60_000)
74
+ except Exception as exc:
75
+ return {"ok": False, "error": str(exc)}
76
+ try:
77
+ workflow = _resolve_workflow_from_vm(vm, name)
78
+ with capture_output(max_stdout_chars=MAX_STDOUT_CHARS) as (run_out, _):
79
+ wf_result = vm.builtin_run_workflow(workflow)
80
+ except Exception as exc:
81
+ return {"ok": False, "error": str(exc)}
82
+ combined_stdout = (def_stdout + run_out.getvalue()).strip()
83
+ wf_data = wf_result if isinstance(wf_result, dict) else {}
84
+ steps = wf_data.get("steps", {})
85
+ out: dict = {"ok": True, "workflow": name, "steps": steps}
86
+ if combined_stdout:
87
+ out["stdout"] = combined_stdout
88
+ return out
89
+
90
+
91
+ def exec_code(runtime: Any, code: str) -> dict:
92
+ result = runtime.run_source(code, timeout_ms=10_000)
93
+ if not result["ok"]:
94
+ err = result.get("error") or {}
95
+ return {"ok": False, "error": err.get("message", "execution failed")}
96
+ return {
97
+ "ok": True,
98
+ "result": result.get("result"),
99
+ "stdout": result.get("stdout", "").strip(),
100
+ }