mcp-nervous-system 1.6.0 → 1.7.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/README.md +2 -2
- package/README.md.bak.1772809790 +172 -0
- package/index.js +340 -1
- package/package.json +1 -1
- package/package.json.bak.1772809772 +42 -0
- package/smoke-test.js +484 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
7 mechanically enforced rules that prevent the most common failure modes when LLMs have access to real infrastructure: context loss, silent failures, file damage, goal drift, and overreach.
|
|
6
6
|
|
|
7
|
-
Built by [Arthur Palyan](https://www.levelsofself.com) at Palyan Family AI System.
|
|
7
|
+
Built by [Arthur Palyan](https://www.levelsofself.com) at Palyan Family AI System. 17 tools including configuration drift detection and emergency kill switch. Battle-tested on an 11-member AI family running 30 processes 24/7 on a single VPS. 58+ violations logged, 0 bypassed.
|
|
8
8
|
|
|
9
9
|
## The Problem
|
|
10
10
|
|
|
@@ -60,7 +60,7 @@ Protocol: MCP 2024-11-05 (Streamable HTTP + SSE)
|
|
|
60
60
|
Authentication: None required
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
## NEW in v1.
|
|
63
|
+
## NEW in v1.6.0
|
|
64
64
|
|
|
65
65
|
**drift_audit** (free tier)
|
|
66
66
|
Configuration drift detection across 5 scopes: roles, versions, files, processes, and website. Scans source-of-truth files (family-roles.json, package.json, UNTOUCHABLE_FILES.txt) against all downstream references - HTML pages, JSON configs, markdown docs, and running PM2 processes. Change one file, drift_audit tells you everywhere else that needs updating. The closed loop that keeps your entire system consistent.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# The Nervous System
|
|
2
|
+
|
|
3
|
+
**LLM Behavioral Enforcement Framework**
|
|
4
|
+
|
|
5
|
+
7 mechanically enforced rules that prevent the most common failure modes when LLMs have access to real infrastructure: context loss, silent failures, file damage, goal drift, and overreach.
|
|
6
|
+
|
|
7
|
+
Built by [Arthur Palyan](https://www.levelsofself.com) at Palyan Family AI System. 16 tools including configuration drift detection and emergency kill switch. Battle-tested on an 11-member AI family running 30 processes 24/7 on a single VPS. 58+ violations logged, 0 bypassed.
|
|
8
|
+
|
|
9
|
+
## The Problem
|
|
10
|
+
|
|
11
|
+
When you give an LLM access to your file system, bash, and production infrastructure, it will eventually:
|
|
12
|
+
|
|
13
|
+
- Edit a file it shouldn't touch
|
|
14
|
+
- Lose context between sessions and start over
|
|
15
|
+
- Drift from the original objective during long tasks
|
|
16
|
+
- Fail silently when a session times out
|
|
17
|
+
- Make logic changes without asking
|
|
18
|
+
- Disappear into debug loops
|
|
19
|
+
|
|
20
|
+
The Nervous System solves all of these with rules enforced by external mechanisms the LLM cannot override.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
### Claude Desktop
|
|
25
|
+
|
|
26
|
+
Add to your `claude_desktop_config.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"nervous-system": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "mcp-nervous-system"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Claude Code
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
claude mcp add nervous-system npx mcp-nervous-system
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Direct
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx mcp-nervous-system
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Server starts on port 3475 with SSE, HTTP, and health endpoints.
|
|
52
|
+
|
|
53
|
+
### Hosted (No Install)
|
|
54
|
+
|
|
55
|
+
The server is live and ready to use:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
URL: https://api.100levelup.com/mcp-ns/
|
|
59
|
+
Protocol: MCP 2024-11-05 (Streamable HTTP + SSE)
|
|
60
|
+
Authentication: None required
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## NEW in v1.5.1
|
|
64
|
+
|
|
65
|
+
**drift_audit** (free tier)
|
|
66
|
+
Configuration drift detection across 5 scopes: roles, versions, files, processes, and website. Scans source-of-truth files (family-roles.json, package.json, UNTOUCHABLE_FILES.txt) against all downstream references - HTML pages, JSON configs, markdown docs, and running PM2 processes. Change one file, drift_audit tells you everywhere else that needs updating. The closed loop that keeps your entire system consistent.
|
|
67
|
+
|
|
68
|
+
**Positioning: Auto mode decides what Claude CAN do. The Nervous System governs HOW it behaves while doing it.**
|
|
69
|
+
|
|
70
|
+
## The 7 Rules
|
|
71
|
+
|
|
72
|
+
| # | Rule | What It Prevents |
|
|
73
|
+
|---|------|-----------------|
|
|
74
|
+
| 1 | **Dispatch Don't Do** | Debug loops, rabbit holes. Tasks > 2 messages get dispatched. |
|
|
75
|
+
| 2 | **Untouchable** | File damage. Protected files mechanically blocked from editing. |
|
|
76
|
+
| 3 | **Write Progress** | Silent failures. Progress noted before each action. |
|
|
77
|
+
| 4 | **Step Back Every 4** | Goal drift. Forced reflection every 4 messages. |
|
|
78
|
+
| 5 | **Delegate and Return** | Invisible work. Background tasks reported immediately. |
|
|
79
|
+
| 6 | **Ask Before Touching** | Unauthorized changes. Logic changes need human approval. |
|
|
80
|
+
| 7 | **Hand Off** | Context loss. Written handoffs every 3-4 exchanges. |
|
|
81
|
+
|
|
82
|
+
## MCP Tools (12)
|
|
83
|
+
|
|
84
|
+
| Tool | Description |
|
|
85
|
+
|------|------------|
|
|
86
|
+
| `get_framework` | Complete framework: all rules, permission protocol, enforcement patterns |
|
|
87
|
+
| `guardrail_rules` | The 7 core rules with triggers, enforcement, and failure modes |
|
|
88
|
+
| `preflight_check` | File protection system: shell script blocks edits to protected files |
|
|
89
|
+
| `session_handoff` | Context preservation: templates for handoff documents |
|
|
90
|
+
| `worklog` | Progress documentation pattern |
|
|
91
|
+
| `violation_logging` | Audit trail: timestamp, type, context for every violation |
|
|
92
|
+
| `step_back_check` | Forced reflection system |
|
|
93
|
+
| `get_nervous_system_info` | System overview and operational stats |
|
|
94
|
+
| `emergency_kill_switch` | Emergency shutdown of all PM2 processes. Requires kill switch secret. Logs to tamper-evident audit trail |
|
|
95
|
+
| `verify_audit_chain` | Walks the SHA-256 hash-chained audit log and verifies every entry. Returns chain integrity status |
|
|
96
|
+
| `dispatch_to_llm` | Spawns a background LLM agent to handle a task. Checks RAM, enforces max 2 concurrent dispatches |
|
|
97
|
+
| `drift_audit` | Configuration drift detection across roles, versions, files, processes, and website. Finds stale values everywhere. |
|
|
98
|
+
|
|
99
|
+
## Kill Switch
|
|
100
|
+
|
|
101
|
+
The `emergency_kill_switch` tool provides an emergency shutdown capability. Send a POST request to `/kill` with the kill switch secret to immediately stop all PM2 processes. Every activation is logged to the tamper-evident audit trail with SHA-256 hash chaining, so kill switch events cannot be hidden or altered after the fact.
|
|
102
|
+
|
|
103
|
+
- Requires authentication (kill switch secret)
|
|
104
|
+
- Logs to hash-chained audit trail
|
|
105
|
+
- Returns confirmation with affected process count
|
|
106
|
+
|
|
107
|
+
## Tamper-Evident Audit Trail
|
|
108
|
+
|
|
109
|
+
Every guardrail violation, kill switch activation, and dispatch event is recorded in a SHA-256 hash-chained audit log. Each entry includes the hash of the previous entry, making it cryptographically impossible to alter or delete past records without breaking the chain.
|
|
110
|
+
|
|
111
|
+
- Use `verify_audit_chain` to walk the entire chain and verify integrity
|
|
112
|
+
- Returns: valid/invalid status, entry count, and break point if tampered
|
|
113
|
+
- 58+ violations logged, 0 bypassed, 0 chain breaks
|
|
114
|
+
|
|
115
|
+
## Dispatch to LLM
|
|
116
|
+
|
|
117
|
+
The `dispatch_to_llm` tool enables a brain + agents architecture. Instead of one LLM session doing everything, complex tasks get dispatched to background agents that run independently under the same 7 rules.
|
|
118
|
+
|
|
119
|
+
- Checks available RAM (requires 500MB+)
|
|
120
|
+
- Enforces max 2 concurrent dispatches
|
|
121
|
+
- Returns PID and log file path for monitoring
|
|
122
|
+
- Every dispatched agent runs under the same nervous system guardrails
|
|
123
|
+
|
|
124
|
+
## EU AI Act Compliance
|
|
125
|
+
|
|
126
|
+
The Nervous System provides practical compliance tools for the EU AI Act. See the full compliance page at:
|
|
127
|
+
|
|
128
|
+
https://api.100levelup.com/family/eu-ai-act.html
|
|
129
|
+
|
|
130
|
+
## Resources (5)
|
|
131
|
+
|
|
132
|
+
- `nervous-system://framework` - The complete framework
|
|
133
|
+
- `nervous-system://quick-start` - Quick start guide
|
|
134
|
+
- `nervous-system://rules` - The 7 core rules
|
|
135
|
+
- `nervous-system://templates` - Templates for handoffs, worklogs, preflight
|
|
136
|
+
- `nervous-system://drift-audit` - Configuration drift detection
|
|
137
|
+
|
|
138
|
+
## Production Stats
|
|
139
|
+
|
|
140
|
+
From the live Palyan Family AI System deployment (Feb 28 - Mar 5, 2026):
|
|
141
|
+
|
|
142
|
+
- **58+** violations caught
|
|
143
|
+
- **29** edits blocked by preflight
|
|
144
|
+
- **13** unique files protected
|
|
145
|
+
- **0** rules bypassed
|
|
146
|
+
- **28** processes monitored
|
|
147
|
+
- **7** days continuous operation
|
|
148
|
+
|
|
149
|
+
## Live Demo
|
|
150
|
+
|
|
151
|
+
Try it yourself (no login required):
|
|
152
|
+
|
|
153
|
+
- **[Interactive Demo](https://api.100levelup.com/family/arthur.html?guest=1)** - Talk to a governed LLM and try to break the rules
|
|
154
|
+
- **[Audit Dashboard](https://api.100levelup.com/family/audit.html)** - See real violation history with timeline
|
|
155
|
+
- **[System Status](https://api.100levelup.com/family/status.html)** - Live health checks
|
|
156
|
+
- **[API Documentation](https://api.100levelup.com/family/api-docs.html)** - Full tool and resource reference
|
|
157
|
+
- **[Case Study](https://api.100levelup.com/family/case-study.html)** - Production deployment data
|
|
158
|
+
- **[Plain English Rules](https://api.100levelup.com/family/rules-plain.html)** - For non-technical stakeholders
|
|
159
|
+
- **[Incident Response](https://api.100levelup.com/family/incident-response.html)** - Detection, containment, resolution
|
|
160
|
+
- **[EU AI Act Compliance](https://api.100levelup.com/family/eu-ai-act.html)** - Practical EU AI Act compliance tools
|
|
161
|
+
|
|
162
|
+
## Philosophy
|
|
163
|
+
|
|
164
|
+
> "LLMs can't reliably self-enforce promises. Guardrails work via preflight.sh, violation logs, and catching drift. Build enforcement systems, don't make promises."
|
|
165
|
+
|
|
166
|
+
If a guardrail can be violated by the thing it guards, it is not a guardrail. It is a suggestion.
|
|
167
|
+
|
|
168
|
+
Every rule in the Nervous System is enforced by an external mechanism: a shell script, a timer, a separate monitoring process. The LLM cannot override, circumvent, or ignore them.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/index.js
CHANGED
|
@@ -715,6 +715,19 @@ const TOOLS = [
|
|
|
715
715
|
}
|
|
716
716
|
}
|
|
717
717
|
}
|
|
718
|
+
},
|
|
719
|
+
// NEW: MCP Analyzer
|
|
720
|
+
{
|
|
721
|
+
name: 'mcp_analyzer',
|
|
722
|
+
annotations: { title: 'MCP Analyzer', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
723
|
+
description: 'Analyzes your project structure and generates a tailored CLAUDE.md with the most useful tools for your workflow. Use mode=analyze to see recommendations, mode=write to generate CLAUDE.md, mode=reload to re-scan and update. Turns the NS from generic tools into a trained assistant that knows your project.',
|
|
724
|
+
inputSchema: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: {
|
|
727
|
+
mode: { type: 'string', enum: ['analyze', 'write', 'reload'], description: 'analyze=show recommendations, write=generate CLAUDE.md, reload=re-scan and update CLAUDE.md' },
|
|
728
|
+
output_path: { type: 'string', description: 'Where to write CLAUDE.md. Defaults to project root.' }
|
|
729
|
+
}
|
|
730
|
+
}
|
|
718
731
|
}
|
|
719
732
|
];
|
|
720
733
|
|
|
@@ -724,7 +737,7 @@ const RESOURCES = [
|
|
|
724
737
|
{ uri: 'nervous-system://quick-start', name: 'Quick Start Guide', description: 'How to implement the nervous system in your own LLM deployment', mimeType: 'text/plain' },
|
|
725
738
|
{ uri: 'nervous-system://rules', name: 'The 7 Core Rules', description: 'All 7 behavioral rules with explanations and enforcement', mimeType: 'text/plain' },
|
|
726
739
|
{ uri: 'nervous-system://templates', name: 'Templates', description: 'Ready-to-use templates for handoffs, worklogs, preflight, and untouchable lists', mimeType: 'text/plain' },
|
|
727
|
-
{ uri: 'nervous-system://drift-audit', name: 'Drift Audit', description: 'Configuration drift detection - checks roles, versions, file references, and running processes against source-of-truth files', mimeType: 'text/plain' }
|
|
740
|
+
{ uri: 'nervous-system://drift-audit', name: 'Drift Audit', description: 'Configuration drift detection - checks roles, versions, file references, and running processes against source-of-truth files', mimeType: 'text/plain' },
|
|
728
741
|
];
|
|
729
742
|
|
|
730
743
|
// ============================================================
|
|
@@ -1891,11 +1904,337 @@ function handleToolCall(name, args) {
|
|
|
1891
1904
|
return runPrePublishAudit(args.source_file);
|
|
1892
1905
|
}
|
|
1893
1906
|
|
|
1907
|
+
case 'mcp_analyzer': {
|
|
1908
|
+
return runMCPAnalyzer(args.mode, args.output_path);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1894
1911
|
default:
|
|
1895
1912
|
return { error: 'Unknown tool' };
|
|
1896
1913
|
}
|
|
1897
1914
|
}
|
|
1898
1915
|
|
|
1916
|
+
// ============================================================
|
|
1917
|
+
// MCP ANALYZER - Analyzes project and generates tailored config
|
|
1918
|
+
// ============================================================
|
|
1919
|
+
|
|
1920
|
+
function runMCPAnalyzer(mode, outputPath) {
|
|
1921
|
+
const result = {
|
|
1922
|
+
timestamp: new Date().toISOString(),
|
|
1923
|
+
mode: mode || 'analyze',
|
|
1924
|
+
recommended_tools: [],
|
|
1925
|
+
claude_md: null,
|
|
1926
|
+
config_suggestions: null
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1929
|
+
// Step 1: Detect project characteristics
|
|
1930
|
+
const projectRoot = PROJECT.project_root || process.cwd();
|
|
1931
|
+
const characteristics = {
|
|
1932
|
+
has_config: !!PROJECT._source && PROJECT._source !== 'defaults',
|
|
1933
|
+
has_protected_files: false,
|
|
1934
|
+
has_html_pages: false,
|
|
1935
|
+
has_pm2: PROJECT.pm2_managed || false,
|
|
1936
|
+
has_package_json: false,
|
|
1937
|
+
has_git: false,
|
|
1938
|
+
has_tests: false,
|
|
1939
|
+
has_docs: false,
|
|
1940
|
+
has_data_dir: !!PROJECT.data_dir,
|
|
1941
|
+
has_logs_dir: !!PROJECT.logs_dir,
|
|
1942
|
+
file_count: 0,
|
|
1943
|
+
js_files: 0,
|
|
1944
|
+
py_files: 0,
|
|
1945
|
+
html_files: 0,
|
|
1946
|
+
json_files: 0,
|
|
1947
|
+
md_files: 0,
|
|
1948
|
+
project_type: 'unknown',
|
|
1949
|
+
languages: [],
|
|
1950
|
+
frameworks: []
|
|
1951
|
+
};
|
|
1952
|
+
|
|
1953
|
+
// Scan project root
|
|
1954
|
+
try {
|
|
1955
|
+
const scanDir = function(dir, depth) {
|
|
1956
|
+
if (depth > 3) return;
|
|
1957
|
+
try {
|
|
1958
|
+
const items = fs.readdirSync(dir);
|
|
1959
|
+
for (const item of items) {
|
|
1960
|
+
if (item.startsWith('.') || item === 'node_modules' || item === '__pycache__') continue;
|
|
1961
|
+
const fullPath = path.join(dir, item);
|
|
1962
|
+
try {
|
|
1963
|
+
const stat = fs.statSync(fullPath);
|
|
1964
|
+
if (stat.isDirectory()) {
|
|
1965
|
+
if (item === 'test' || item === 'tests' || item === '__tests__') characteristics.has_tests = true;
|
|
1966
|
+
if (item === 'docs' || item === 'documentation') characteristics.has_docs = true;
|
|
1967
|
+
if (item === 'public' || item === 'static' || item === 'html') characteristics.has_html_pages = true;
|
|
1968
|
+
scanDir(fullPath, depth + 1);
|
|
1969
|
+
} else {
|
|
1970
|
+
characteristics.file_count++;
|
|
1971
|
+
if (item.endsWith('.js') || item.endsWith('.ts')) characteristics.js_files++;
|
|
1972
|
+
if (item.endsWith('.py')) characteristics.py_files++;
|
|
1973
|
+
if (item.endsWith('.html')) characteristics.html_files++;
|
|
1974
|
+
if (item.endsWith('.json')) characteristics.json_files++;
|
|
1975
|
+
if (item.endsWith('.md')) characteristics.md_files++;
|
|
1976
|
+
if (item === 'package.json') characteristics.has_package_json = true;
|
|
1977
|
+
if (item === '.gitignore' || item === '.git') characteristics.has_git = true;
|
|
1978
|
+
if (item === 'CLAUDE.md' || item === 'claude.md') characteristics.has_docs = true;
|
|
1979
|
+
}
|
|
1980
|
+
} catch (e) {}
|
|
1981
|
+
}
|
|
1982
|
+
} catch (e) {}
|
|
1983
|
+
};
|
|
1984
|
+
scanDir(projectRoot, 0);
|
|
1985
|
+
} catch (e) {}
|
|
1986
|
+
|
|
1987
|
+
// Check for protected files list
|
|
1988
|
+
const protectedPath = projectPath('protected_files_list');
|
|
1989
|
+
if (protectedPath) {
|
|
1990
|
+
try {
|
|
1991
|
+
const content = fs.readFileSync(protectedPath, 'utf8');
|
|
1992
|
+
const lines = content.split('\n').filter(l => l.trim() && !l.trim().startsWith('#'));
|
|
1993
|
+
characteristics.has_protected_files = lines.length > 0;
|
|
1994
|
+
characteristics.protected_count = lines.length;
|
|
1995
|
+
} catch (e) {}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// Check for git
|
|
1999
|
+
try {
|
|
2000
|
+
if (fs.existsSync(path.join(projectRoot, '.git'))) characteristics.has_git = true;
|
|
2001
|
+
} catch (e) {}
|
|
2002
|
+
|
|
2003
|
+
// Check for PM2
|
|
2004
|
+
if (!characteristics.has_pm2) {
|
|
2005
|
+
try {
|
|
2006
|
+
const { execSync } = require('child_process');
|
|
2007
|
+
execSync('pm2 jlist 2>/dev/null', { timeout: 5000 });
|
|
2008
|
+
characteristics.has_pm2 = true;
|
|
2009
|
+
} catch (e) {}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Determine project type
|
|
2013
|
+
if (characteristics.js_files > 0) characteristics.languages.push('javascript');
|
|
2014
|
+
if (characteristics.py_files > 0) characteristics.languages.push('python');
|
|
2015
|
+
|
|
2016
|
+
if (characteristics.has_pm2 && characteristics.js_files > 5) {
|
|
2017
|
+
characteristics.project_type = 'production_system';
|
|
2018
|
+
} else if (characteristics.has_html_pages && characteristics.js_files > 0) {
|
|
2019
|
+
characteristics.project_type = 'web_application';
|
|
2020
|
+
} else if (characteristics.has_package_json && characteristics.js_files > 0) {
|
|
2021
|
+
characteristics.project_type = 'node_project';
|
|
2022
|
+
} else if (characteristics.py_files > 0) {
|
|
2023
|
+
characteristics.project_type = 'python_project';
|
|
2024
|
+
} else if (characteristics.md_files > 2) {
|
|
2025
|
+
characteristics.project_type = 'documentation';
|
|
2026
|
+
} else {
|
|
2027
|
+
characteristics.project_type = 'general';
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Don't expose raw project internals - just use them for recommendations
|
|
2031
|
+
result.project_type = characteristics.project_type;
|
|
2032
|
+
result.has_config = characteristics.has_config;
|
|
2033
|
+
|
|
2034
|
+
// Step 2: Recommend tools based on project type
|
|
2035
|
+
const recommendations = [];
|
|
2036
|
+
|
|
2037
|
+
// Universal tools everyone needs
|
|
2038
|
+
recommendations.push({
|
|
2039
|
+
tool: 'get_framework',
|
|
2040
|
+
priority: 'essential',
|
|
2041
|
+
reason: 'Core behavioral rules. Read this first to understand how the NS works.'
|
|
2042
|
+
});
|
|
2043
|
+
recommendations.push({
|
|
2044
|
+
tool: 'preflight_check',
|
|
2045
|
+
priority: 'essential',
|
|
2046
|
+
reason: 'Protects critical files from accidental edits. Set up your protected files list.'
|
|
2047
|
+
});
|
|
2048
|
+
recommendations.push({
|
|
2049
|
+
tool: 'worklog',
|
|
2050
|
+
priority: 'essential',
|
|
2051
|
+
reason: 'Prevents silent failures. Write progress before every action.'
|
|
2052
|
+
});
|
|
2053
|
+
recommendations.push({
|
|
2054
|
+
tool: 'session_handoff',
|
|
2055
|
+
priority: 'essential',
|
|
2056
|
+
reason: 'Solves context loss between sessions. Update the handoff before ending work.'
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
// Conditional tools
|
|
2060
|
+
if (characteristics.has_protected_files || characteristics.file_count > 20) {
|
|
2061
|
+
recommendations.push({
|
|
2062
|
+
tool: 'guardrail_rules',
|
|
2063
|
+
priority: 'high',
|
|
2064
|
+
reason: 'Protected files detected. These rules enforce discipline around file edits.'
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (characteristics.project_type === 'production_system') {
|
|
2069
|
+
recommendations.push({
|
|
2070
|
+
tool: 'drift_audit',
|
|
2071
|
+
priority: 'high',
|
|
2072
|
+
reason: 'Production system detected. Drift audit catches when configs, docs, and running processes go out of sync.'
|
|
2073
|
+
});
|
|
2074
|
+
recommendations.push({
|
|
2075
|
+
tool: 'security_audit',
|
|
2076
|
+
priority: 'high',
|
|
2077
|
+
reason: 'Production system detected. Security audit catches exposed secrets and misconfigurations.'
|
|
2078
|
+
});
|
|
2079
|
+
recommendations.push({
|
|
2080
|
+
tool: 'emergency_kill_switch',
|
|
2081
|
+
priority: 'medium',
|
|
2082
|
+
reason: 'Production system with PM2. Kill switch gives you emergency shutdown capability.'
|
|
2083
|
+
});
|
|
2084
|
+
recommendations.push({
|
|
2085
|
+
tool: 'dispatch_to_llm',
|
|
2086
|
+
priority: 'high',
|
|
2087
|
+
reason: 'Complex system. Dispatch heavy tasks to background agents instead of blocking your main session.'
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (characteristics.has_pm2) {
|
|
2092
|
+
recommendations.push({
|
|
2093
|
+
tool: 'session_close',
|
|
2094
|
+
priority: 'high',
|
|
2095
|
+
reason: 'PM2 processes detected. Session close runs full audit + propagation before you end work.'
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
if (characteristics.has_html_pages) {
|
|
2100
|
+
recommendations.push({
|
|
2101
|
+
tool: 'page_health',
|
|
2102
|
+
priority: 'medium',
|
|
2103
|
+
reason: 'HTML pages detected. Page health catches broken links, missing meta tags, and UX issues.'
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
if (characteristics.has_package_json && characteristics.has_git) {
|
|
2108
|
+
recommendations.push({
|
|
2109
|
+
tool: 'pre_publish_audit',
|
|
2110
|
+
priority: 'high',
|
|
2111
|
+
reason: 'Publishable package detected. Pre-publish audit catches secrets and hardcoded paths before they ship.'
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
recommendations.push({
|
|
2116
|
+
tool: 'step_back_check',
|
|
2117
|
+
priority: 'medium',
|
|
2118
|
+
reason: 'Forces reflection every few messages. Prevents tunnel vision on details.'
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
recommendations.push({
|
|
2122
|
+
tool: 'verify_audit_chain',
|
|
2123
|
+
priority: 'low',
|
|
2124
|
+
reason: 'Tamper-proof audit trail. Verifies no one has modified the activity log.'
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
// Sort by priority
|
|
2128
|
+
const priorityOrder = { essential: 0, high: 1, medium: 2, low: 3 };
|
|
2129
|
+
recommendations.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
2130
|
+
|
|
2131
|
+
result.recommended_tools = recommendations;
|
|
2132
|
+
|
|
2133
|
+
// Step 3: Generate CLAUDE.md
|
|
2134
|
+
const essentialTools = recommendations.filter(r => r.priority === 'essential').map(r => r.tool);
|
|
2135
|
+
const highTools = recommendations.filter(r => r.priority === 'high').map(r => r.tool);
|
|
2136
|
+
const mediumTools = recommendations.filter(r => r.priority === 'medium').map(r => r.tool);
|
|
2137
|
+
|
|
2138
|
+
let claudeMd = '# CLAUDE.md - Generated by The Nervous System MCP Analyzer\n';
|
|
2139
|
+
claudeMd += '# Project type: ' + characteristics.project_type + '\n';
|
|
2140
|
+
claudeMd += '# Generated: ' + result.timestamp + '\n';
|
|
2141
|
+
claudeMd += '# Re-run: mcp_analyzer mode=reload to refresh\n\n';
|
|
2142
|
+
|
|
2143
|
+
claudeMd += '## BEHAVIORAL RULES\n\n';
|
|
2144
|
+
claudeMd += 'This project uses The Nervous System for LLM behavioral enforcement.\n';
|
|
2145
|
+
claudeMd += 'Before doing anything, internalize these rules:\n\n';
|
|
2146
|
+
claudeMd += '1. DISPATCH DONT DO - If a task takes 2+ messages, write a task file and dispatch it.\n';
|
|
2147
|
+
claudeMd += '2. UNTOUCHABLE = UNTOUCHABLE - Run preflight before ANY file edit. If blocked, STOP.\n';
|
|
2148
|
+
claudeMd += '3. WRITE PROGRESS AS YOU GO - Note what you are about to do before each action.\n';
|
|
2149
|
+
claudeMd += '4. STEP BACK EVERY 4 MESSAGES - Are we solving the real problem?\n';
|
|
2150
|
+
claudeMd += '5. ASK BEFORE TOUCHING - Do not modify configs or processes without permission.\n';
|
|
2151
|
+
claudeMd += '6. HAND OFF EVERY FEW MESSAGES - Update the session handoff file.\n\n';
|
|
2152
|
+
|
|
2153
|
+
if (characteristics.has_protected_files) {
|
|
2154
|
+
claudeMd += '## PROTECTED FILES\n\n';
|
|
2155
|
+
claudeMd += 'This project has a protected files list at: ' + (protectedPath || 'PROTECTED_FILES.txt') + '\n';
|
|
2156
|
+
claudeMd += 'Run preflight_check before editing ANY file. If it is protected, STOP and ask.\n\n';
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
claudeMd += '## YOUR TOOLS (in order of importance)\n\n';
|
|
2160
|
+
claudeMd += '### Always use these:\n';
|
|
2161
|
+
for (const t of essentialTools) {
|
|
2162
|
+
const rec = recommendations.find(r => r.tool === t);
|
|
2163
|
+
claudeMd += '- **' + t + '** - ' + rec.reason + '\n';
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
if (highTools.length > 0) {
|
|
2167
|
+
claudeMd += '\n### Use regularly:\n';
|
|
2168
|
+
for (const t of highTools) {
|
|
2169
|
+
const rec = recommendations.find(r => r.tool === t);
|
|
2170
|
+
claudeMd += '- **' + t + '** - ' + rec.reason + '\n';
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
if (mediumTools.length > 0) {
|
|
2175
|
+
claudeMd += '\n### Use when relevant:\n';
|
|
2176
|
+
for (const t of mediumTools) {
|
|
2177
|
+
const rec = recommendations.find(r => r.tool === t);
|
|
2178
|
+
claudeMd += '- **' + t + '** - ' + rec.reason + '\n';
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
claudeMd += '\n## SESSION WORKFLOW\n\n';
|
|
2183
|
+
claudeMd += '1. Start: Read session handoff file to see where the last session left off\n';
|
|
2184
|
+
claudeMd += '2. Work: Write progress before each action. Run preflight before file edits.\n';
|
|
2185
|
+
if (characteristics.has_pm2) {
|
|
2186
|
+
claudeMd += '3. End: Call session_close (runs drift audit + propagators automatically)\n';
|
|
2187
|
+
} else {
|
|
2188
|
+
claudeMd += '3. End: Update the session handoff file with what was done and what is next\n';
|
|
2189
|
+
}
|
|
2190
|
+
claudeMd += '4. Every 4 messages: Call step_back_check to verify direction\n\n';
|
|
2191
|
+
|
|
2192
|
+
if (characteristics.project_type === 'production_system') {
|
|
2193
|
+
claudeMd += '## PRODUCTION RULES\n\n';
|
|
2194
|
+
claudeMd += 'This is a production system. Extra care required:\n';
|
|
2195
|
+
claudeMd += '- Run security_audit after any auth or config changes\n';
|
|
2196
|
+
claudeMd += '- Run drift_audit after any file changes\n';
|
|
2197
|
+
claudeMd += '- Run pre_publish_audit before publishing any packages\n';
|
|
2198
|
+
claudeMd += '- Use dispatch_to_llm for tasks that take 2+ messages\n';
|
|
2199
|
+
claudeMd += '- Kill switch is available for emergencies\n\n';
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// Only include CLAUDE.md content in write/reload mode, not analyze
|
|
2203
|
+
if (mode === 'write' || mode === 'reload') {
|
|
2204
|
+
result.claude_md_generated = true;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// Step 4: Config suggestions (if no config exists)
|
|
2208
|
+
if (!characteristics.has_config) {
|
|
2209
|
+
result.setup_needed = true;
|
|
2210
|
+
result.setup_message = 'No nervous-system.config.json found. Run with mode=write to create one and generate a tailored CLAUDE.md for your project.';
|
|
2211
|
+
} else {
|
|
2212
|
+
result.setup_needed = false;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Step 5: If mode is 'reload' or 'write', write the CLAUDE.md
|
|
2216
|
+
if ((mode === 'reload' || mode === 'write') && outputPath) {
|
|
2217
|
+
try {
|
|
2218
|
+
fs.writeFileSync(outputPath, claudeMd);
|
|
2219
|
+
result.written_to = outputPath;
|
|
2220
|
+
} catch (e) {
|
|
2221
|
+
result.write_error = e.message;
|
|
2222
|
+
}
|
|
2223
|
+
} else if (mode === 'reload' || mode === 'write') {
|
|
2224
|
+
// Default output path
|
|
2225
|
+
const defaultPath = path.join(projectRoot, 'CLAUDE.md');
|
|
2226
|
+
try {
|
|
2227
|
+
fs.writeFileSync(defaultPath, claudeMd);
|
|
2228
|
+
result.written_to = defaultPath;
|
|
2229
|
+
} catch (e) {
|
|
2230
|
+
result.write_error = e.message;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
return result;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
|
|
1899
2238
|
// Handle resource reads
|
|
1900
2239
|
function handleResourceRead(uri) {
|
|
1901
2240
|
switch (uri) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-nervous-system",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "The Nervous System - LLM Behavioral Enforcement Framework. 7 mechanically enforced rules, 17 tools including kill switch, audit chain, dispatch, drift audit, security audit, page health, pre-publish audit, and session close. MCP server for Claude Desktop, Claude Code, and any MCP-compatible client.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-nervous-system",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "The Nervous System - LLM Behavioral Enforcement Framework. 7 mechanically enforced rules, 17 tools including kill switch, audit chain, dispatch, drift audit, security audit, page health, pre-publish audit, and session close. MCP server for Claude Desktop, Claude Code, and any MCP-compatible client.",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-nervous-system": "./stdio.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node server.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"model-context-protocol",
|
|
15
|
+
"llm",
|
|
16
|
+
"ai-safety",
|
|
17
|
+
"ai-governance",
|
|
18
|
+
"behavioral-enforcement",
|
|
19
|
+
"guardrails",
|
|
20
|
+
"claude",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"nervous-system",
|
|
23
|
+
"kill-switch",
|
|
24
|
+
"audit-trail"
|
|
25
|
+
],
|
|
26
|
+
"author": "Arthur Palyan <palyanai@gmail.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/levelsofself/mcp-nervous-system"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://api.100levelup.com/family/gateway.html",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/levelsofself/mcp-nervous-system/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/smoke-test.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// NS Smoke Test - Regression testing for The Nervous System MCP Server
|
|
3
|
+
// Run after every restart, before every publish, and daily via Tamara
|
|
4
|
+
// Usage: node ns-smoke-test.js [port] [--pre-publish] [--verbose]
|
|
5
|
+
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { execSync, spawn } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const PORT = parseInt(args.find(a => /^\d+$/.test(a))) || 3475;
|
|
13
|
+
const PRE_PUBLISH = args.includes('--pre-publish');
|
|
14
|
+
const VERBOSE = args.includes('--verbose');
|
|
15
|
+
const FRESH_INSTALL = args.includes('--fresh-install');
|
|
16
|
+
|
|
17
|
+
// All 16 tools with minimal valid arguments
|
|
18
|
+
const TOOL_TESTS = [
|
|
19
|
+
{ name: 'get_framework', args: {}, expectFields: ['name', 'version', 'core_rules'] },
|
|
20
|
+
{ name: 'get_nervous_system_info', args: { topic: 'overview' }, expectType: 'string' },
|
|
21
|
+
{ name: 'guardrail_rules', args: { rule: 'all' }, expectType: 'object' },
|
|
22
|
+
{ name: 'step_back_check', args: { context: 'smoke test' }, expectFields: ['trigger', 'steps'] },
|
|
23
|
+
{ name: 'preflight_check', args: { action: 'get_script' }, expectType: 'string' },
|
|
24
|
+
{ name: 'violation_logging', args: { action: 'get_pattern' }, expectType: 'string' },
|
|
25
|
+
{ name: 'worklog', args: { action: 'get_template' }, expectType: 'string' },
|
|
26
|
+
{ name: 'session_handoff', args: { action: 'get_template' }, expectType: 'string' },
|
|
27
|
+
{ name: 'drift_audit', args: { scope: 'roles' }, expectFields: ['status', 'drift_count'] },
|
|
28
|
+
{ name: 'security_audit', args: {}, expectFields: ['status'] },
|
|
29
|
+
{ name: 'page_health', args: { page: 'all' }, expectType: 'object' },
|
|
30
|
+
{ name: 'verify_audit_chain', args: {}, expectType: 'object' },
|
|
31
|
+
{ name: 'auto_propagate', args: {}, expectType: 'object' },
|
|
32
|
+
{ name: 'session_close', args: {}, expectType: 'object' },
|
|
33
|
+
// dispatch_to_llm - skip in smoke test (spawns real agent)
|
|
34
|
+
// emergency_kill_switch - skip in smoke test (stops everything)
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Tools that should NOT be called in smoke tests
|
|
38
|
+
const SKIP_TOOLS = ['dispatch_to_llm', 'emergency_kill_switch'];
|
|
39
|
+
|
|
40
|
+
let passed = 0;
|
|
41
|
+
let failed = 0;
|
|
42
|
+
let skipped = 0;
|
|
43
|
+
const failures = [];
|
|
44
|
+
|
|
45
|
+
function log(msg) { console.log(msg); }
|
|
46
|
+
function vlog(msg) { if (VERBOSE) console.log(' ' + msg); }
|
|
47
|
+
|
|
48
|
+
function callMCP(method, params) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const body = JSON.stringify({
|
|
51
|
+
jsonrpc: '2.0',
|
|
52
|
+
id: Date.now(),
|
|
53
|
+
method: method,
|
|
54
|
+
params: params || {}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const req = http.request({
|
|
58
|
+
hostname: 'localhost',
|
|
59
|
+
port: PORT,
|
|
60
|
+
path: '/mcp',
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
63
|
+
timeout: 15000
|
|
64
|
+
}, (res) => {
|
|
65
|
+
let data = '';
|
|
66
|
+
res.on('data', c => data += c);
|
|
67
|
+
res.on('end', () => {
|
|
68
|
+
try {
|
|
69
|
+
resolve(JSON.parse(data));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
reject(new Error('Invalid JSON response: ' + data.substring(0, 200)));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
req.on('error', reject);
|
|
76
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
77
|
+
req.write(body);
|
|
78
|
+
req.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function testHealth() {
|
|
83
|
+
log('\n[1/5] HEALTH CHECK');
|
|
84
|
+
try {
|
|
85
|
+
const resp = await new Promise((resolve, reject) => {
|
|
86
|
+
const req = http.get({ hostname: 'localhost', port: PORT, path: '/health', timeout: 5000 }, (res) => {
|
|
87
|
+
let data = '';
|
|
88
|
+
res.on('data', c => data += c);
|
|
89
|
+
res.on('end', () => {
|
|
90
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
91
|
+
catch (e) { resolve({ status: res.statusCode, body: data }); }
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
req.on('error', reject);
|
|
95
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (resp.status === 200) {
|
|
99
|
+
passed++;
|
|
100
|
+
log(' PASS: /health returns 200');
|
|
101
|
+
vlog('Response: ' + JSON.stringify(resp.body));
|
|
102
|
+
} else {
|
|
103
|
+
failed++;
|
|
104
|
+
failures.push({ test: 'health', error: 'Status ' + resp.status });
|
|
105
|
+
log(' FAIL: /health returns ' + resp.status);
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
failed++;
|
|
109
|
+
failures.push({ test: 'health', error: e.message });
|
|
110
|
+
log(' FAIL: /health - ' + e.message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function testInitialize() {
|
|
115
|
+
log('\n[2/5] MCP INITIALIZE');
|
|
116
|
+
try {
|
|
117
|
+
const resp = await callMCP('initialize', {});
|
|
118
|
+
if (resp.result && resp.result.serverInfo && resp.result.capabilities) {
|
|
119
|
+
passed++;
|
|
120
|
+
log(' PASS: initialize returns valid server info');
|
|
121
|
+
vlog('Server: ' + resp.result.serverInfo.name + ' v' + resp.result.serverInfo.version);
|
|
122
|
+
vlog('Capabilities: ' + Object.keys(resp.result.capabilities).join(', '));
|
|
123
|
+
} else {
|
|
124
|
+
failed++;
|
|
125
|
+
failures.push({ test: 'initialize', error: 'Missing serverInfo or capabilities' });
|
|
126
|
+
log(' FAIL: initialize - bad response structure');
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
failed++;
|
|
130
|
+
failures.push({ test: 'initialize', error: e.message });
|
|
131
|
+
log(' FAIL: initialize - ' + e.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function testToolsList() {
|
|
136
|
+
log('\n[3/5] TOOLS LIST');
|
|
137
|
+
try {
|
|
138
|
+
const resp = await callMCP('tools/list', {});
|
|
139
|
+
if (resp.result && resp.result.tools && Array.isArray(resp.result.tools)) {
|
|
140
|
+
const toolNames = resp.result.tools.map(t => t.name);
|
|
141
|
+
passed++;
|
|
142
|
+
log(' PASS: tools/list returns ' + toolNames.length + ' tools');
|
|
143
|
+
vlog('Tools: ' + toolNames.join(', '));
|
|
144
|
+
|
|
145
|
+
// Verify expected tools exist
|
|
146
|
+
const expectedTools = TOOL_TESTS.map(t => t.name).concat(SKIP_TOOLS);
|
|
147
|
+
const missing = expectedTools.filter(t => !toolNames.includes(t));
|
|
148
|
+
if (missing.length > 0) {
|
|
149
|
+
failed++;
|
|
150
|
+
failures.push({ test: 'tools_list_completeness', error: 'Missing tools: ' + missing.join(', ') });
|
|
151
|
+
log(' FAIL: Missing expected tools: ' + missing.join(', '));
|
|
152
|
+
} else {
|
|
153
|
+
passed++;
|
|
154
|
+
log(' PASS: All expected tools present');
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
failed++;
|
|
158
|
+
failures.push({ test: 'tools_list', error: 'Invalid response' });
|
|
159
|
+
log(' FAIL: tools/list - invalid response');
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
failed++;
|
|
163
|
+
failures.push({ test: 'tools_list', error: e.message });
|
|
164
|
+
log(' FAIL: tools/list - ' + e.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function testEachTool() {
|
|
169
|
+
log('\n[4/5] INDIVIDUAL TOOL CALLS');
|
|
170
|
+
for (const test of TOOL_TESTS) {
|
|
171
|
+
try {
|
|
172
|
+
const resp = await callMCP('tools/call', { name: test.name, arguments: test.args });
|
|
173
|
+
|
|
174
|
+
// Check for rate limit (not a real failure)
|
|
175
|
+
if (resp.result && resp.result.content && resp.result.content[0] &&
|
|
176
|
+
resp.result.content[0].text && (resp.result.content[0].text.includes('rate_limit') || resp.result.content[0].text.includes('authentication_required'))) {
|
|
177
|
+
skipped++;
|
|
178
|
+
log(' SKIP: ' + test.name + ' (rate limited)');
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for valid response
|
|
183
|
+
if (resp.error) {
|
|
184
|
+
failed++;
|
|
185
|
+
failures.push({ test: test.name, error: resp.error.message || JSON.stringify(resp.error) });
|
|
186
|
+
log(' FAIL: ' + test.name + ' - ' + (resp.error.message || 'error'));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!resp.result || !resp.result.content || !resp.result.content[0]) {
|
|
191
|
+
failed++;
|
|
192
|
+
failures.push({ test: test.name, error: 'Empty or invalid response structure' });
|
|
193
|
+
log(' FAIL: ' + test.name + ' - empty response');
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const text = resp.result.content[0].text;
|
|
198
|
+
let parsed;
|
|
199
|
+
try { parsed = JSON.parse(text); } catch (e) { parsed = text; }
|
|
200
|
+
|
|
201
|
+
// Validate expected shape
|
|
202
|
+
if (test.expectFields) {
|
|
203
|
+
const missingFields = test.expectFields.filter(f => !(f in parsed));
|
|
204
|
+
if (missingFields.length > 0) {
|
|
205
|
+
failed++;
|
|
206
|
+
failures.push({ test: test.name, error: 'Missing fields: ' + missingFields.join(', ') });
|
|
207
|
+
log(' FAIL: ' + test.name + ' - missing fields: ' + missingFields.join(', '));
|
|
208
|
+
} else {
|
|
209
|
+
passed++;
|
|
210
|
+
log(' PASS: ' + test.name);
|
|
211
|
+
vlog('Fields present: ' + test.expectFields.join(', '));
|
|
212
|
+
}
|
|
213
|
+
} else if (test.expectType === 'string') {
|
|
214
|
+
if (typeof parsed === 'string' && parsed.length > 0) {
|
|
215
|
+
passed++;
|
|
216
|
+
log(' PASS: ' + test.name + ' (' + parsed.length + ' chars)');
|
|
217
|
+
} else if (typeof parsed === 'object') {
|
|
218
|
+
// Some tools return JSON even when we expect string, that is fine
|
|
219
|
+
passed++;
|
|
220
|
+
log(' PASS: ' + test.name + ' (object response)');
|
|
221
|
+
} else {
|
|
222
|
+
failed++;
|
|
223
|
+
failures.push({ test: test.name, error: 'Expected string, got ' + typeof parsed });
|
|
224
|
+
log(' FAIL: ' + test.name + ' - expected string');
|
|
225
|
+
}
|
|
226
|
+
} else if (test.expectType === 'object') {
|
|
227
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
228
|
+
passed++;
|
|
229
|
+
log(' PASS: ' + test.name);
|
|
230
|
+
} else {
|
|
231
|
+
failed++;
|
|
232
|
+
failures.push({ test: test.name, error: 'Expected object, got ' + typeof parsed });
|
|
233
|
+
log(' FAIL: ' + test.name + ' - expected object');
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
passed++;
|
|
237
|
+
log(' PASS: ' + test.name + ' (response received)');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
} catch (e) {
|
|
241
|
+
failed++;
|
|
242
|
+
failures.push({ test: test.name, error: e.message });
|
|
243
|
+
log(' FAIL: ' + test.name + ' - ' + e.message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Note skipped dangerous tools
|
|
248
|
+
for (const skip of SKIP_TOOLS) {
|
|
249
|
+
skipped++;
|
|
250
|
+
log(' SKIP: ' + skip + ' (dangerous in test)');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function testSourceCode() {
|
|
255
|
+
log('\n[5/5] SOURCE CODE CHECKS');
|
|
256
|
+
|
|
257
|
+
// Find the NS source file
|
|
258
|
+
const candidates = [
|
|
259
|
+
path.join(__dirname, 'index.js'),
|
|
260
|
+
'/root/mcp-nervous-system.js',
|
|
261
|
+
path.join(process.cwd(), 'index.js'),
|
|
262
|
+
];
|
|
263
|
+
let sourceFile = null;
|
|
264
|
+
for (const c of candidates) {
|
|
265
|
+
if (fs.existsSync(c)) { sourceFile = c; break; }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!sourceFile) {
|
|
269
|
+
skipped++;
|
|
270
|
+
log(' SKIP: Could not find NS source file');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const content = fs.readFileSync(sourceFile, 'utf8');
|
|
275
|
+
|
|
276
|
+
// Check for leaked passwords (common patterns)
|
|
277
|
+
const passwordPatterns = [
|
|
278
|
+
/Liarzhek/i,
|
|
279
|
+
/Levelsofself1/i,
|
|
280
|
+
];
|
|
281
|
+
let passwordClean = true;
|
|
282
|
+
for (const pat of passwordPatterns) {
|
|
283
|
+
if (pat.test(content)) {
|
|
284
|
+
failed++;
|
|
285
|
+
passwordClean = false;
|
|
286
|
+
failures.push({ test: 'password_check', error: 'Password pattern found: ' + pat.source });
|
|
287
|
+
log(' FAIL: Password found in source: ' + pat.source);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (passwordClean) {
|
|
291
|
+
passed++;
|
|
292
|
+
log(' PASS: No known passwords in source');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check for hardcoded paths (only in non-description/non-comment lines)
|
|
296
|
+
if (PRE_PUBLISH) {
|
|
297
|
+
const lines = content.split('\n');
|
|
298
|
+
let hardcodedPaths = 0;
|
|
299
|
+
lines.forEach((line, idx) => {
|
|
300
|
+
if (line.trim().startsWith('//')) return;
|
|
301
|
+
if (line.includes('description:') || line.includes('context:') || line.includes('tagline:')) return;
|
|
302
|
+
if (line.includes("'/root/") || line.includes('"/root/')) {
|
|
303
|
+
hardcodedPaths++;
|
|
304
|
+
vlog('Line ' + (idx + 1) + ': ' + line.trim().substring(0, 80));
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
if (hardcodedPaths > 0) {
|
|
308
|
+
// Warning not failure -- v1.5.2 still has these, v1.6.0 will fix
|
|
309
|
+
log(' WARN: ' + hardcodedPaths + ' hardcoded /root/ paths (fix in v1.6.0)');
|
|
310
|
+
} else {
|
|
311
|
+
passed++;
|
|
312
|
+
log(' PASS: No hardcoded /root/ paths');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Syntax check
|
|
317
|
+
try {
|
|
318
|
+
execSync('node -c ' + sourceFile + ' 2>&1');
|
|
319
|
+
passed++;
|
|
320
|
+
log(' PASS: Syntax check (node -c)');
|
|
321
|
+
} catch (e) {
|
|
322
|
+
failed++;
|
|
323
|
+
failures.push({ test: 'syntax_check', error: 'Syntax error in ' + sourceFile });
|
|
324
|
+
log(' FAIL: Syntax error in source');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function testFreshInstall() {
|
|
329
|
+
log('\n[BONUS] FRESH INSTALL TEST');
|
|
330
|
+
const tmpDir = '/tmp/ns-fresh-test-' + Date.now();
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
// Create temp directory and install from npm
|
|
334
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
335
|
+
log(' Installing mcp-nervous-system in ' + tmpDir + '...');
|
|
336
|
+
execSync('cd ' + tmpDir + ' && npm init -y > /dev/null 2>&1 && npm install mcp-nervous-system > /dev/null 2>&1', { timeout: 60000 });
|
|
337
|
+
|
|
338
|
+
// Find the installed index.js
|
|
339
|
+
const indexPath = path.join(tmpDir, 'node_modules', 'mcp-nervous-system', 'index.js');
|
|
340
|
+
if (!fs.existsSync(indexPath)) {
|
|
341
|
+
failed++;
|
|
342
|
+
failures.push({ test: 'fresh_install', error: 'index.js not found after npm install' });
|
|
343
|
+
log(' FAIL: index.js not found');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Syntax check the installed file
|
|
348
|
+
try {
|
|
349
|
+
execSync('node -c ' + indexPath + ' 2>&1');
|
|
350
|
+
passed++;
|
|
351
|
+
log(' PASS: Fresh install syntax check');
|
|
352
|
+
} catch (e) {
|
|
353
|
+
failed++;
|
|
354
|
+
failures.push({ test: 'fresh_install_syntax', error: 'Syntax error in fresh install' });
|
|
355
|
+
log(' FAIL: Fresh install has syntax error');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Check for passwords in installed file
|
|
359
|
+
const content = fs.readFileSync(indexPath, 'utf8');
|
|
360
|
+
if (/Liarzhek|Levelsofself1/i.test(content)) {
|
|
361
|
+
failed++;
|
|
362
|
+
failures.push({ test: 'fresh_install_passwords', error: 'Passwords still in npm package' });
|
|
363
|
+
log(' FAIL: Passwords found in fresh npm install!');
|
|
364
|
+
} else {
|
|
365
|
+
passed++;
|
|
366
|
+
log(' PASS: No passwords in fresh npm install');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Start the server on a random port and test health
|
|
370
|
+
const testPort = 19475 + Math.floor(Math.random() * 1000);
|
|
371
|
+
const stdioPth = path.join(tmpDir, 'node_modules', 'mcp-nervous-system', 'server.js');
|
|
372
|
+
if (fs.existsSync(stdioPth)) {
|
|
373
|
+
// Patch port for testing
|
|
374
|
+
const serverContent = fs.readFileSync(stdioPth, 'utf8');
|
|
375
|
+
const patchedContent = serverContent.replace(/const PORT = \d+/, 'const PORT = ' + testPort);
|
|
376
|
+
const patchedPath = path.join(tmpDir, 'test-server.js');
|
|
377
|
+
fs.writeFileSync(patchedPath, patchedContent);
|
|
378
|
+
|
|
379
|
+
const proc = spawn('node', [patchedPath], {
|
|
380
|
+
cwd: tmpDir,
|
|
381
|
+
stdio: 'ignore',
|
|
382
|
+
detached: true,
|
|
383
|
+
env: { ...process.env, PORT: testPort.toString() }
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Wait for startup
|
|
387
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const healthResp = await new Promise((resolve, reject) => {
|
|
391
|
+
const req = http.get({ hostname: 'localhost', port: testPort, path: '/health', timeout: 5000 }, (res) => {
|
|
392
|
+
let data = '';
|
|
393
|
+
res.on('data', c => data += c);
|
|
394
|
+
res.on('end', () => resolve({ status: res.statusCode }));
|
|
395
|
+
});
|
|
396
|
+
req.on('error', reject);
|
|
397
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (healthResp.status === 200) {
|
|
401
|
+
passed++;
|
|
402
|
+
log(' PASS: Fresh install server starts and responds on port ' + testPort);
|
|
403
|
+
} else {
|
|
404
|
+
failed++;
|
|
405
|
+
log(' FAIL: Fresh install server returned status ' + healthResp.status);
|
|
406
|
+
}
|
|
407
|
+
} catch (e) {
|
|
408
|
+
// Server might not start without dependencies - that is OK to note
|
|
409
|
+
skipped++;
|
|
410
|
+
log(' SKIP: Fresh install server could not start (' + e.message + ')');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Cleanup
|
|
414
|
+
try { process.kill(-proc.pid); } catch (e) {}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
} catch (e) {
|
|
418
|
+
failed++;
|
|
419
|
+
failures.push({ test: 'fresh_install', error: e.message });
|
|
420
|
+
log(' FAIL: Fresh install test - ' + e.message);
|
|
421
|
+
} finally {
|
|
422
|
+
// Cleanup
|
|
423
|
+
try { execSync('rm -rf ' + tmpDir + ' 2>/dev/null'); } catch (e) {}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function run() {
|
|
428
|
+
log('===========================================');
|
|
429
|
+
log(' NERVOUS SYSTEM SMOKE TEST');
|
|
430
|
+
log(' Port: ' + PORT);
|
|
431
|
+
log(' Mode: ' + (PRE_PUBLISH ? 'PRE-PUBLISH' : FRESH_INSTALL ? 'FRESH INSTALL' : 'STANDARD'));
|
|
432
|
+
log(' Time: ' + new Date().toISOString());
|
|
433
|
+
log('===========================================');
|
|
434
|
+
|
|
435
|
+
await testHealth();
|
|
436
|
+
await testInitialize();
|
|
437
|
+
await testToolsList();
|
|
438
|
+
await testEachTool();
|
|
439
|
+
await testSourceCode();
|
|
440
|
+
|
|
441
|
+
if (FRESH_INSTALL || PRE_PUBLISH) {
|
|
442
|
+
await testFreshInstall();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
log('\n===========================================');
|
|
446
|
+
log(' RESULTS');
|
|
447
|
+
log('===========================================');
|
|
448
|
+
log(' Passed: ' + passed);
|
|
449
|
+
log(' Failed: ' + failed);
|
|
450
|
+
log(' Skipped: ' + skipped);
|
|
451
|
+
log(' Total: ' + (passed + failed + skipped));
|
|
452
|
+
|
|
453
|
+
if (failures.length > 0) {
|
|
454
|
+
log('\n FAILURES:');
|
|
455
|
+
failures.forEach(f => log(' - ' + f.test + ': ' + f.error));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const status = failed === 0 ? 'ALL PASS' : 'FAILURES DETECTED';
|
|
459
|
+
log('\n STATUS: ' + status);
|
|
460
|
+
log('===========================================');
|
|
461
|
+
|
|
462
|
+
// Write results to log file for Tamara
|
|
463
|
+
const resultFile = '/tmp/ns-smoke-test-result.json';
|
|
464
|
+
const result = {
|
|
465
|
+
timestamp: new Date().toISOString(),
|
|
466
|
+
port: PORT,
|
|
467
|
+
mode: PRE_PUBLISH ? 'pre-publish' : FRESH_INSTALL ? 'fresh-install' : 'standard',
|
|
468
|
+
passed: passed,
|
|
469
|
+
failed: failed,
|
|
470
|
+
skipped: skipped,
|
|
471
|
+
status: status,
|
|
472
|
+
failures: failures
|
|
473
|
+
};
|
|
474
|
+
try {
|
|
475
|
+
fs.writeFileSync(resultFile, JSON.stringify(result, null, 2));
|
|
476
|
+
} catch (e) {}
|
|
477
|
+
|
|
478
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
run().catch(e => {
|
|
482
|
+
console.error('Smoke test crashed: ' + e.message);
|
|
483
|
+
process.exit(2);
|
|
484
|
+
});
|