strest-mcp 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christopher Hailfinger (Diamantenschmiede)
4
+ Original work Copyright (c) 2018-2023 Eyke Rehbein
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,151 @@
1
- # Temporary Holding Version
1
+ # ⚡ Strest MCP (Strest Orchestrator)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+ [![Glama Score](https://glama.ai/mcp/servers/tastenkasperle/strest-mcp/badges/score.svg)](https://glama.ai/mcp/servers/tastenkasperle/strest-mcp)
5
+ [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-green.svg)](https://modelcontextprotocol.io)
6
+ [![Tests](https://img.shields.io/badge/Tests-3%2F3%20Passed-brightgreen.svg)](tests/)
7
+ [![Security Guard](https://img.shields.io/badge/Security-Raptor_Guard_Hardened-orange.svg)](src/core/guard.mjs)
8
+ [![Zero Dependencies](https://img.shields.io/badge/Dependencies-Zero-blue.svg)](package.json)
9
+
10
+ > **Headless Declarative Multi-Step REST API Test & Chaining Engine for Autonomous AI Agents and DevSecOps.**
11
+ > *Execute complex, multi-stage API workflows (Auth ➔ Token Extraction ➔ Resource Creation ➔ Assertions) in a single deterministic MCP tool call.*
12
+
13
+ ---
14
+
15
+ ## 💡 The Problem
16
+
17
+ Autonomous AI agents (Claude Code, Cursor, Antigravity, AutoGen, CrewAI) and CI pipelines frequently need to interact with multi-step REST APIs.
18
+ When executing chained requests step-by-step with individual `curl` commands:
19
+
20
+ 1. **Context Window Waste & Token Bloat:** Passing whole JSON payloads back and forth across multiple LLM turns wastes hundreds of tokens.
21
+ 2. **Fragile State Propagation:** Extracting tokens, session IDs, and foreign keys across separate prompts introduces hallucinations and state loss.
22
+ 3. **SSRF & Security Hazards:** AI agents given raw network access can be coerced into probing internal cloud metadata endpoints (`169.254.169.254`).
23
+
24
+ **Strest MCP solves this:** You pass a declarative multi-step scenario definition to the engine. It executes all steps sequentially with in-memory variable chaining, enforces strict assertions, applies active SSRF defense, and returns a clean, structured execution report in a single round-trip.
25
+
26
+ ---
27
+
28
+ ## ⚡ Key Capabilities
29
+
30
+ * 🔗 **Declarative Multi-Step Chaining:** Chain outputs from previous steps into downstream headers, query params, or JSON bodies using `<$ steps.<step_name>.body.<property> $>`.
31
+ * 🛡️ **Built-in SSRF & Cloud Metadata Shield:** Proactively blocks attacks against AWS/GCP/Azure instance metadata endpoints (`169.254.169.254`, `metadata.google.internal`), local loopbacks, and dangerous protocols (`file://`, `ftp://`).
32
+ * 🪶 **Zero External Dependencies:** Built entirely with native Node.js 20+ `fetch`, keeping the package footprint lightweight and lightning-fast.
33
+ * 🤖 **Model Context Protocol (MCP) Native:** Integrates seamlessly via stdio JSON-RPC 2.0 with Claude Desktop, Cursor, Antigravity, and any MCP-compliant client.
34
+ * 🎯 **Rich Assertions:** Status code validation, regex header checks, deep JSON body assertions, and timing thresholds.
35
+
36
+ ---
37
+
38
+ ## 🤖 Model Context Protocol (MCP) Setup
39
+
40
+ ### Add to Claude Desktop / Cursor / Antigravity
41
+
42
+ Add the following to your `claude_desktop_config.json` or MCP settings:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "strest-mcp": {
48
+ "command": "npx",
49
+ "args": ["-y", "strest-mcp"]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ Or run directly from a local clone:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "strest-mcp": {
61
+ "command": "node",
62
+ "args": ["/path/to/strest_orchestrator/src/mcp/server.mjs"]
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### Available MCP Tools
69
+
70
+ | Tool | Description | Input Parameters |
71
+ | :--- | :--- | :--- |
72
+ | `strest_run_scenario` | Executes a declarative multi-step REST API workflow scenario with automatic variable chaining and schema assertions. | `scenario` (object) OR `scenarioPath` (string) |
73
+ | `strest_template` | Generates a ready-to-use template for multi-step authentication and CRUD chaining. | `baseUrl` (optional string, e.g. `http://localhost:5000`) |
74
+
75
+ ---
76
+
77
+ ## 💻 CLI Usage
78
+
79
+ You can also run Strest Orchestrator as a standalone CLI tool:
80
+
81
+ ```bash
82
+ # Display help and usage
83
+ npx strest help
84
+
85
+ # Run an API workflow scenario from a JSON file
86
+ npx strest run scenarios/auth_and_crud.json
87
+
88
+ # Generate a starter template
89
+ npx strest template > scenario.json
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 📋 Scenario Example (Chaining in Action)
95
+
96
+ ```json
97
+ {
98
+ "name": "Auth and User Project Creation",
99
+ "steps": [
100
+ {
101
+ "name": "login",
102
+ "url": "https://api.example.com/v1/auth/login",
103
+ "method": "POST",
104
+ "headers": { "Content-Type": "application/json" },
105
+ "body": { "username": "admin", "password": "SuperSecretPassword" },
106
+ "assert": {
107
+ "status": 200,
108
+ "body": { "token": "defined" }
109
+ }
110
+ },
111
+ {
112
+ "name": "create_project",
113
+ "url": "https://api.example.com/v1/projects",
114
+ "method": "POST",
115
+ "headers": {
116
+ "Content-Type": "application/json",
117
+ "Authorization": "Bearer <$ steps.login.body.token $>"
118
+ },
119
+ "body": {
120
+ "title": "Autonomous Agent Workspace",
121
+ "requestId": "<$ faker.uuid $>"
122
+ },
123
+ "assert": {
124
+ "status": 201,
125
+ "body": { "id": "defined" }
126
+ }
127
+ }
128
+ ]
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ ## 🛡️ Security & Hardening (Raptor Guard v2.0)
135
+
136
+ * **SSRF Prevention:** Blocks requests to IPv4/IPv6 link-local addresses, link-local multicast, and known cloud metadata IP ranges (`169.254.169.254`, `100.100.100.200`).
137
+ * **Path Traversal Shield:** Resolves and validates scenario file paths against path traversal sequences (`../`, null bytes, system files).
138
+ * **Hermetic Execution:** 100% offline self-testing suite with embedded ephemeral mock servers.
139
+
140
+ ---
141
+
142
+ ## 📜 Origin & Credits
143
+
144
+ * Based on the concept of [`eykrehbein/strest`](https://github.com/eykrehbein/strest) (1.7k+ ⭐, MIT).
145
+ * Modernized, decoupled, SSRF-hardened, and transformed into an autonomous Model Context Protocol server by Christopher Hailfinger ([@tastenkasperle](https://github.com/tastenkasperle)) as part of the **Diamantenschmiede** initiative.
146
+
147
+ ---
148
+
149
+ ## 📄 License
150
+
151
+ MIT License — see [LICENSE](LICENSE) for details.
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "strest-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Declarative Multi-Step REST API Test & Chaining Engine for AI Agents and DevSecOps",
5
+ "main": "src/index.mjs",
6
+ "bin": {
7
+ "strest": "src/cli/index.mjs",
8
+ "strest-mcp": "src/mcp/server.mjs"
9
+ },
10
+ "type": "module",
11
+ "scripts": {
12
+ "test": "node tests/run_all.mjs",
13
+ "start": "node src/mcp/server.mjs",
14
+ "run": "node src/cli/index.mjs run"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "strest",
20
+ "api-testing",
21
+ "rest-client",
22
+ "chaining",
23
+ "ai-agents",
24
+ "devsecops",
25
+ "ssrf-protection",
26
+ "automation"
27
+ ],
28
+ "author": "Christopher Hailfinger (Diamantenschmiede)",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/tastenkasperle/strest-mcp.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/tastenkasperle/strest-mcp/issues"
36
+ },
37
+ "homepage": "https://github.com/tastenkasperle/strest-mcp#readme"
38
+ }
package/server.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "name": "strest-mcp",
4
+ "version": "1.0.0",
5
+ "description": "Declarative Multi-Step REST API Test & Chaining Engine for AI Agents and DevSecOps",
6
+ "author": {
7
+ "name": "Diamantenschmiede",
8
+ "url": "https://github.com/tastenkasperle"
9
+ },
10
+ "homepage": "https://github.com/tastenkasperle/strest-mcp",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/tastenkasperle/strest-mcp.git"
14
+ },
15
+ "capabilities": {
16
+ "tools": true
17
+ },
18
+ "transport": {
19
+ "type": "stdio",
20
+ "command": "node",
21
+ "args": [
22
+ "src/mcp/server.mjs"
23
+ ]
24
+ }
25
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,6 @@
1
+ startCommand:
2
+ type: stdio
3
+ config:
4
+ command: node
5
+ args:
6
+ - src/mcp/server.mjs
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Strest Standalone CLI Interface
4
+ * Part of Diamantenschmiede (AYA Protocol)
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import { executeScenario } from '../core/executor.mjs';
9
+ import { SecurityGuard } from '../core/guard.mjs';
10
+
11
+ function printHelp() {
12
+ console.log(`
13
+ ⚡ Strest Orchestrator CLI – Headless Multi-Step REST Engine
14
+
15
+ VERWENDUNG:
16
+ strest <command> [options]
17
+
18
+ BEFEHLE:
19
+ run <scenario.json> Führt ein mehrstufiges API-Szenario aus
20
+ Optionen: --bail (Stop bei erstem Fehler, Standard: true)
21
+
22
+ template Gibt ein Beispiel-Szenario auf stdout aus
23
+
24
+ help Zeigt diese Hilfe an
25
+ `);
26
+ }
27
+
28
+ async function handleRun(args) {
29
+ const filePath = args[0];
30
+ if (!filePath) {
31
+ console.error('❌ Fehler: Bitte Pfad zur scenario.json angeben.');
32
+ process.exit(1);
33
+ }
34
+
35
+ const safePath = SecurityGuard.validateSafePath(filePath);
36
+ if (!fs.existsSync(safePath)) {
37
+ console.error(`❌ Fehler: Datei nicht gefunden: ${safePath}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ const raw = fs.readFileSync(safePath, 'utf-8');
42
+ const scenario = JSON.parse(raw);
43
+
44
+ console.log(`🚀 Führe Szenario aus: '${scenario.name || safePath}'...`);
45
+ const result = await executeScenario(scenario);
46
+
47
+ console.log('\n--- ERGEBNIS ---');
48
+ console.log(`Status: ${result.success ? '✅ ERFOLGREICH' : '❌ FEHLGESCHLAGEN'}`);
49
+ console.log(`Schritte: ${result.executedSteps} / ${result.totalSteps}`);
50
+ console.log(`Dauer: ${result.durationMs}ms\n`);
51
+
52
+ for (const step of result.steps) {
53
+ const symbol = step.success ? '✔' : '✖';
54
+ console.log(`${symbol} [${step.request.method}] ${step.stepName} (${step.response?.status || 'ERR'} in ${step.response?.durationMs || 0}ms)`);
55
+ if (!step.success && step.errors) {
56
+ for (const err of step.errors) {
57
+ console.log(` └─ 🚨 ${err}`);
58
+ }
59
+ }
60
+ }
61
+
62
+ if (!result.success) {
63
+ process.exit(1);
64
+ }
65
+ }
66
+
67
+ function handleTemplate() {
68
+ const template = {
69
+ name: "Sample Multi-Step Flow",
70
+ steps: [
71
+ {
72
+ name: "login",
73
+ url: "http://localhost:3000/api/login",
74
+ method: "POST",
75
+ body: { username: "admin", password: "password" },
76
+ assert: { status: 200, body: { token: "defined" } }
77
+ },
78
+ {
79
+ name: "get_user",
80
+ url: "http://localhost:3000/api/users/me",
81
+ method: "GET",
82
+ headers: { authorization: "Bearer <$ steps.login.body.token $>" },
83
+ assert: { status: 200 }
84
+ }
85
+ ]
86
+ };
87
+ console.log(JSON.stringify(template, null, 2));
88
+ }
89
+
90
+ async function main() {
91
+ const argv = process.argv.slice(2);
92
+ const command = argv[0];
93
+ const args = argv.slice(1);
94
+
95
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
96
+ printHelp();
97
+ return;
98
+ }
99
+
100
+ switch (command) {
101
+ case 'run':
102
+ await handleRun(args);
103
+ break;
104
+ case 'template':
105
+ handleTemplate();
106
+ break;
107
+ default:
108
+ console.error(`❌ Unbekannter Befehl: ${command}`);
109
+ printHelp();
110
+ process.exit(1);
111
+ }
112
+ }
113
+
114
+ main().catch((err) => {
115
+ console.error('❌ Fehler:', err.message);
116
+ process.exit(1);
117
+ });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Strest Assertion Validator
3
+ * Part of Diamantenschmiede (AYA Protocol)
4
+ */
5
+
6
+ import { getByPath } from './interpolator.mjs';
7
+
8
+ /**
9
+ * Validates step response against assertions
10
+ * @param {Object} response { status: number, headers: {}, body: any }
11
+ * @param {Object} assertions
12
+ * @returns {{ passed: boolean, errors: string[] }}
13
+ */
14
+ export function validateAssertions(response, assertions = {}) {
15
+ const errors = [];
16
+
17
+ // 1. Status Code Check
18
+ if (assertions.status !== undefined) {
19
+ if (response.status !== assertions.status) {
20
+ errors.push(`Expected HTTP status ${assertions.status} but got ${response.status}`);
21
+ }
22
+ }
23
+
24
+ // 2. Status Range Check (e.g. statusRange: [200, 299])
25
+ if (Array.isArray(assertions.statusRange) && assertions.statusRange.length === 2) {
26
+ const [min, max] = assertions.statusRange;
27
+ if (response.status < min || response.status > max) {
28
+ errors.push(`Status ${response.status} outside expected range [${min}, ${max}]`);
29
+ }
30
+ }
31
+
32
+ // 3. Header Checks
33
+ if (assertions.headers && typeof assertions.headers === 'object') {
34
+ for (const [key, expectedVal] of Object.entries(assertions.headers)) {
35
+ const actualVal = response.headers[key.toLowerCase()];
36
+ if (actualVal === undefined) {
37
+ errors.push(`Expected header '${key}' is missing`);
38
+ } else if (expectedVal !== '*' && !String(actualVal).includes(String(expectedVal))) {
39
+ errors.push(`Header '${key}' value '${actualVal}' does not match '${expectedVal}'`);
40
+ }
41
+ }
42
+ }
43
+
44
+ // 4. Body Property Assertions (e.g. body: { "token": "string", "user.id": 42 })
45
+ if (assertions.body && typeof assertions.body === 'object') {
46
+ for (const [propPath, expectedVal] of Object.entries(assertions.body)) {
47
+ const actualVal = getByPath(response.body, propPath);
48
+
49
+ if (expectedVal === 'defined') {
50
+ if (actualVal === undefined || actualVal === null) {
51
+ errors.push(`Body property '${propPath}' must be defined`);
52
+ }
53
+ } else if (expectedVal === 'string') {
54
+ if (typeof actualVal !== 'string') {
55
+ errors.push(`Body property '${propPath}' must be a string, got ${typeof actualVal}`);
56
+ }
57
+ } else if (expectedVal === 'number') {
58
+ if (typeof actualVal !== 'number') {
59
+ errors.push(`Body property '${propPath}' must be a number, got ${typeof actualVal}`);
60
+ }
61
+ } else if (actualVal !== expectedVal) {
62
+ errors.push(`Body property '${propPath}' expected '${expectedVal}' but got '${actualVal}'`);
63
+ }
64
+ }
65
+ }
66
+
67
+ return {
68
+ passed: errors.length === 0,
69
+ errors
70
+ };
71
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Strest Headless Execution Engine
3
+ * Part of Diamantenschmiede (AYA Protocol)
4
+ */
5
+
6
+ import { interpolateDeep } from './interpolator.mjs';
7
+ import { validateAssertions } from './assert.mjs';
8
+ import { SecurityGuard } from './guard.mjs';
9
+
10
+ const MAX_REDIRECTS = 5;
11
+
12
+ /**
13
+ * Executes a single HTTP step with SSRF-safe redirect handling and timeout protection
14
+ * @param {Object} stepDefinition
15
+ * @param {Object} context { steps: {}, env: {} }
16
+ * @param {Object} options
17
+ * @returns {Promise<Object>} stepResult
18
+ */
19
+ export async function executeStep(stepDefinition, context = {}, options = {}) {
20
+ // 1. Interpolate request definition with accumulated context
21
+ const step = interpolateDeep(stepDefinition, context);
22
+
23
+ const url = step.url;
24
+ const method = (step.method || 'GET').toUpperCase();
25
+ const headers = step.headers || {};
26
+ const body = step.body;
27
+ const allowLocalhost = options.allowLocalhost ?? true;
28
+ const timeoutMs = options.timeoutMs || step.timeoutMs || 15000;
29
+
30
+ // Validate initial URL with SSRF Guard
31
+ SecurityGuard.validateUrl(url, { allowLocalhost });
32
+
33
+ const fetchOptions = {
34
+ method,
35
+ headers: { ...headers },
36
+ redirect: 'manual',
37
+ signal: AbortSignal.timeout(timeoutMs)
38
+ };
39
+
40
+ if (body !== undefined && method !== 'GET' && method !== 'HEAD') {
41
+ if (typeof body === 'object') {
42
+ fetchOptions.body = JSON.stringify(body);
43
+ const hasContentType = Object.keys(fetchOptions.headers).some(
44
+ k => k.toLowerCase() === 'content-type'
45
+ );
46
+ if (!hasContentType) {
47
+ fetchOptions.headers['content-type'] = 'application/json';
48
+ }
49
+ } else {
50
+ fetchOptions.body = String(body);
51
+ }
52
+ }
53
+
54
+ const startTime = Date.now();
55
+ let response;
56
+ let parsedBody;
57
+
58
+ try {
59
+ let currentUrl = url;
60
+ let currentOptions = { ...fetchOptions };
61
+
62
+ for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++) {
63
+ response = await fetch(currentUrl, currentOptions);
64
+
65
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
66
+ const location = response.headers.get('location');
67
+ if (!location) break;
68
+ if (redirectCount === MAX_REDIRECTS) {
69
+ throw new Error(`Maximum redirect limit (${MAX_REDIRECTS}) exceeded`);
70
+ }
71
+ const resolvedRedirect = new URL(location, currentUrl).toString();
72
+ // Validate every redirect hop against SSRF Guard
73
+ SecurityGuard.validateUrl(resolvedRedirect, { allowLocalhost });
74
+ currentUrl = resolvedRedirect;
75
+ if (response.status === 303 || ((response.status === 301 || response.status === 302) && currentOptions.method === 'POST')) {
76
+ currentOptions = { ...currentOptions, method: 'GET', body: undefined };
77
+ }
78
+ continue;
79
+ }
80
+ break;
81
+ }
82
+
83
+ const durationMs = Date.now() - startTime;
84
+
85
+ const responseHeaders = {};
86
+ for (const [k, v] of response.headers.entries()) {
87
+ responseHeaders[k.toLowerCase()] = v;
88
+ }
89
+
90
+ const contentType = responseHeaders['content-type'] || '';
91
+ if (contentType.includes('application/json')) {
92
+ parsedBody = await response.json();
93
+ } else {
94
+ parsedBody = await response.text();
95
+ }
96
+
97
+ const responseObj = {
98
+ status: response.status,
99
+ statusText: response.statusText,
100
+ headers: responseHeaders,
101
+ body: parsedBody,
102
+ durationMs
103
+ };
104
+
105
+ // 2. Validate Assertions
106
+ const assertionResult = validateAssertions(responseObj, step.assert || {});
107
+
108
+ return {
109
+ stepName: step.name || step.id || 'anonymous_step',
110
+ success: assertionResult.passed,
111
+ errors: assertionResult.errors,
112
+ request: { method, url, headers: fetchOptions.headers, body },
113
+ response: responseObj
114
+ };
115
+ } catch (err) {
116
+ return {
117
+ stepName: step.name || step.id || 'anonymous_step',
118
+ success: false,
119
+ errors: [err.message],
120
+ request: { method, url, headers: fetchOptions.headers, body },
121
+ response: null
122
+ };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Executes a multi-step scenario
128
+ * @param {Object} scenario { name: string, steps: Array<Object>, env: {} }
129
+ * @param {Object} options
130
+ * @returns {Promise<Object>} scenarioResult
131
+ */
132
+ export async function executeScenario(scenario, options = {}) {
133
+ const steps = scenario.steps || [];
134
+ const context = {
135
+ steps: {},
136
+ env: { ...(scenario.env || {}), ...(options.env || {}) },
137
+ allowProcessEnv: options.allowProcessEnv === true
138
+ };
139
+
140
+ const results = [];
141
+ let scenarioSuccess = true;
142
+ const startTime = Date.now();
143
+
144
+ for (const stepDef of steps) {
145
+ const stepResult = await executeStep(stepDef, context, options);
146
+ results.push(stepResult);
147
+
148
+ // Save step response into context for downstream steps
149
+ const stepKey = stepDef.name || stepDef.id;
150
+ if (stepKey) {
151
+ context.steps[stepKey] = stepResult.response;
152
+ }
153
+
154
+ if (!stepResult.success) {
155
+ scenarioSuccess = false;
156
+ if (options.bail !== false) {
157
+ // Stop execution on first failed step (bail mode)
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ return {
164
+ scenarioName: scenario.name || 'Unnamed Scenario',
165
+ success: scenarioSuccess,
166
+ totalSteps: steps.length,
167
+ executedSteps: results.length,
168
+ durationMs: Date.now() - startTime,
169
+ steps: results
170
+ };
171
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Strest SecurityGuard v2.0
3
+ * SSRF & Malicious Input Defense (AYA Protocol / Fire Team Elite Certified)
4
+ */
5
+
6
+ import path from 'path';
7
+
8
+ // Dangerous SSRF / Cloud Metadata Hosts
9
+ const METADATA_HOSTS = new Set([
10
+ '169.254.169.254', // AWS / Azure / OpenStack IMDSv1/v2
11
+ 'metadata.google.internal', // GCP Metadata
12
+ 'metadata',
13
+ '100.100.100.200', // Alibaba Cloud Metadata
14
+ 'instance-data',
15
+ '[fd00:ec2::254]', // AWS IPv6 IMDS
16
+ 'fd00:ec2::254'
17
+ ]);
18
+
19
+ export class SecurityGuard {
20
+ /**
21
+ * Validates target URL against SSRF and Cloud Metadata Exfiltration
22
+ * @param {string} rawUrl
23
+ * @param {Object} options
24
+ * @param {boolean} [options.allowLocalhost=true] Allow testing local dev servers (e.g. localhost:5000, 127.0.0.1)
25
+ * @returns {URL} Validated URL object
26
+ */
27
+ static validateUrl(rawUrl, options = { allowLocalhost: true }) {
28
+ if (!rawUrl || typeof rawUrl !== 'string') {
29
+ throw new Error('Security Violation: Invalid URL provided');
30
+ }
31
+
32
+ let parsed;
33
+ try {
34
+ parsed = new URL(rawUrl);
35
+ } catch (e) {
36
+ throw new Error(`Security Violation: Malformed URL: ${rawUrl}`);
37
+ }
38
+
39
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
40
+ throw new Error(`Security Violation: Prohibited protocol '${parsed.protocol}'. Only http/https allowed.`);
41
+ }
42
+
43
+ const hostname = parsed.hostname.toLowerCase();
44
+
45
+ // Check Cloud Metadata Endpoints & Link-Local Ranges (Strict block, cannot be bypassed)
46
+ if (
47
+ METADATA_HOSTS.has(hostname) ||
48
+ hostname.startsWith('169.254.') ||
49
+ hostname.includes('ffff:169.254.') ||
50
+ hostname.includes('ffff:a9fe:a9fe')
51
+ ) {
52
+ throw new Error(`Security Violation: Access to cloud instance metadata service (${hostname}) is blocked.`);
53
+ }
54
+
55
+ if (!options.allowLocalhost) {
56
+ if (
57
+ hostname === 'localhost' ||
58
+ hostname === '127.0.0.1' ||
59
+ hostname === '::1' ||
60
+ hostname === '[::1]' ||
61
+ hostname === '0.0.0.0' ||
62
+ hostname.startsWith('127.')
63
+ ) {
64
+ throw new Error('Security Violation: Localhost access restricted in this environment.');
65
+ }
66
+ }
67
+
68
+ return parsed;
69
+ }
70
+
71
+ /**
72
+ * Validates safe file path against path traversal and sensitive credential exposure
73
+ * @param {string} targetPath
74
+ * @param {string} allowedBaseDir
75
+ * @returns {string}
76
+ */
77
+ static validateSafePath(targetPath, allowedBaseDir = process.cwd()) {
78
+ if (!targetPath || typeof targetPath !== 'string') {
79
+ throw new Error('Security Violation: Invalid path argument');
80
+ }
81
+
82
+ if (targetPath.includes('\0')) {
83
+ throw new Error('Security Violation: Null byte detected in path');
84
+ }
85
+
86
+ const base = path.resolve(allowedBaseDir);
87
+ const resolved = path.resolve(base, targetPath);
88
+ const basename = path.basename(resolved);
89
+
90
+ // Block sensitive secrets first
91
+ if (
92
+ /^\.env(\..+)?$/i.test(basename) ||
93
+ /^id_(rsa|ed25519|ecdsa)/i.test(basename) ||
94
+ /masterpw/i.test(basename) ||
95
+ /credentials/i.test(basename)
96
+ ) {
97
+ throw new Error(`Security Violation: Protected credential file blocked: ${basename}`);
98
+ }
99
+
100
+ // Strict containment check inside allowedBaseDir
101
+ const rel = path.relative(base, resolved);
102
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
103
+ throw new Error(`Security Violation: Path traversal outside allowed workspace blocked: ${targetPath}`);
104
+ }
105
+
106
+ return resolved;
107
+ }
108
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Strest Data Interpolator & Chaining Engine
3
+ * Part of Diamantenschmiede (AYA Protocol)
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+
8
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
9
+
10
+ /**
11
+ * Resolves dot-notated path in an object (e.g. "steps.login.body.token")
12
+ * @param {Object} obj
13
+ * @param {string} path
14
+ * @returns {any}
15
+ */
16
+ export function getByPath(obj, path) {
17
+ if (!obj || !path || typeof path !== 'string') return undefined;
18
+ const parts = path.split('.');
19
+ let current = obj;
20
+ for (const part of parts) {
21
+ if (current === null || current === undefined) return undefined;
22
+ if (FORBIDDEN_KEYS.has(part)) return undefined;
23
+ if (typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part)) {
24
+ return undefined;
25
+ }
26
+ current = current[part];
27
+ }
28
+ return current;
29
+ }
30
+
31
+ /**
32
+ * Generates synthetic helper data
33
+ */
34
+ export const SYNTHETIC_HELPERS = {
35
+ uuid: () => crypto.randomUUID(),
36
+ timestamp: () => Date.now(),
37
+ isoDate: () => new Date().toISOString(),
38
+ randomInt: (min = 1, max = 1000) => Math.floor(Math.random() * (max - min + 1)) + min
39
+ };
40
+
41
+ /**
42
+ * Interpolates string with `<$ expression $>` patterns
43
+ * @param {string} template
44
+ * @param {Object} context { steps: {}, env: {} }
45
+ * @returns {any}
46
+ */
47
+ export function interpolateString(template, context = {}) {
48
+ if (typeof template !== 'string') return template;
49
+
50
+ // Single exact match replacement (preserving numbers/objects/booleans)
51
+ const singleMatch = template.match(/^<\$\s*([a-zA-Z0-9_.]+)\s*\$>$/);
52
+ if (singleMatch) {
53
+ const expr = singleMatch[1];
54
+ return resolveExpression(expr, context);
55
+ }
56
+
57
+ // Multi-match substring replacement
58
+ return template.replace(/<\$\s*([a-zA-Z0-9_.]+)\s*\$>/g, (_, expr) => {
59
+ const val = resolveExpression(expr, context);
60
+ return val !== undefined ? String(val) : '';
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Resolves expression against context or helpers
66
+ * @param {string} expr
67
+ * @param {Object} context
68
+ * @returns {any}
69
+ */
70
+ function resolveExpression(expr, context) {
71
+ if (expr.startsWith('faker.') || expr.startsWith('helper.')) {
72
+ const helperName = expr.split('.')[1];
73
+ if (Object.prototype.hasOwnProperty.call(SYNTHETIC_HELPERS, helperName)) {
74
+ return SYNTHETIC_HELPERS[helperName]();
75
+ }
76
+ }
77
+
78
+ if (expr.startsWith('env.')) {
79
+ const envKey = expr.slice(4);
80
+ if (FORBIDDEN_KEYS.has(envKey)) return undefined;
81
+ if (context?.env && Object.prototype.hasOwnProperty.call(context.env, envKey)) {
82
+ return context.env[envKey];
83
+ }
84
+ if (context?.allowProcessEnv === true) {
85
+ return process.env[envKey];
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ return getByPath(context, expr);
91
+ }
92
+
93
+ /**
94
+ * Recursively interpolates objects, arrays, and strings
95
+ * @param {any} target
96
+ * @param {Object} context
97
+ * @returns {any}
98
+ */
99
+ export function interpolateDeep(target, context = {}) {
100
+ if (typeof target === 'string') {
101
+ return interpolateString(target, context);
102
+ }
103
+
104
+ if (Array.isArray(target)) {
105
+ return target.map(item => interpolateDeep(item, context));
106
+ }
107
+
108
+ if (target !== null && typeof target === 'object') {
109
+ const result = {};
110
+ for (const [key, value] of Object.entries(target)) {
111
+ if (FORBIDDEN_KEYS.has(key)) continue;
112
+ result[key] = interpolateDeep(value, context);
113
+ }
114
+ return result;
115
+ }
116
+
117
+ return target;
118
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Strest Orchestrator - Main Package Entry Point
3
+ * Part of Diamantenschmiede (AYA Protocol)
4
+ */
5
+
6
+ export { executeScenario, executeStep } from './core/executor.mjs';
7
+ export {
8
+ interpolateDeep,
9
+ interpolateDeep as interpolate,
10
+ interpolateString,
11
+ getByPath,
12
+ SYNTHETIC_HELPERS
13
+ } from './core/interpolator.mjs';
14
+ export {
15
+ validateAssertions,
16
+ validateAssertions as assertResponse
17
+ } from './core/assert.mjs';
18
+ export { SecurityGuard } from './core/guard.mjs';
19
+ export { handleRequest } from './mcp/server.mjs';
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Strest MCP Server (Model Context Protocol)
4
+ * Native Stdio JSON-RPC 2.0 Server for Local AI Agents
5
+ * Part of Diamantenschmiede (AYA Protocol / Gang 3)
6
+ */
7
+
8
+ import readline from 'readline';
9
+ import fs from 'fs';
10
+ import { executeScenario } from '../core/executor.mjs';
11
+ import { SecurityGuard } from '../core/guard.mjs';
12
+
13
+ const SERVER_NAME = 'strest-mcp';
14
+ const SERVER_VERSION = '1.0.0';
15
+
16
+ const TOOLS = [
17
+ {
18
+ name: 'strest_run_scenario',
19
+ description: 'Executes a declarative multi-step REST API workflow scenario with automatic variable chaining and schema assertions.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ scenario: {
24
+ type: 'object',
25
+ description: 'The scenario definition object containing steps and assertions'
26
+ },
27
+ scenarioPath: {
28
+ type: 'string',
29
+ description: 'Path to a scenario JSON file on disk'
30
+ }
31
+ }
32
+ }
33
+ },
34
+ {
35
+ name: 'strest_template',
36
+ description: 'Generates a ready-to-use template for multi-step authentication and CRUD chaining.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ baseUrl: { type: 'string', description: 'Base URL for the scenario (e.g. http://localhost:5000)' }
41
+ }
42
+ }
43
+ }
44
+ ];
45
+
46
+ export async function handleRequest(request) {
47
+ const { id, method, params } = request;
48
+
49
+ if (
50
+ method === 'notifications/initialized' ||
51
+ method === 'initialized' ||
52
+ (typeof method === 'string' && method.startsWith('notifications/')) ||
53
+ id === undefined
54
+ ) {
55
+ return null;
56
+ }
57
+
58
+ if (method === 'initialize') {
59
+ return {
60
+ jsonrpc: '2.0',
61
+ id,
62
+ result: {
63
+ protocolVersion: '2024-11-05',
64
+ capabilities: { tools: {} },
65
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
66
+ }
67
+ };
68
+ }
69
+
70
+ if (method === 'ping') {
71
+ return {
72
+ jsonrpc: '2.0',
73
+ id,
74
+ result: {}
75
+ };
76
+ }
77
+
78
+ if (method === 'tools/list') {
79
+ return {
80
+ jsonrpc: '2.0',
81
+ id,
82
+ result: { tools: TOOLS }
83
+ };
84
+ }
85
+
86
+ if (method === 'tools/call') {
87
+ const { name, arguments: args = {} } = params;
88
+
89
+ try {
90
+ if (name === 'strest_run_scenario') {
91
+ let scenarioObj;
92
+ if (args.scenario) {
93
+ scenarioObj = args.scenario;
94
+ } else if (args.scenarioPath) {
95
+ const safePath = SecurityGuard.validateSafePath(args.scenarioPath);
96
+ scenarioObj = JSON.parse(fs.readFileSync(safePath, 'utf-8'));
97
+ } else {
98
+ throw new Error('Either scenario object or scenarioPath must be provided');
99
+ }
100
+
101
+ const result = await executeScenario(scenarioObj);
102
+ return {
103
+ jsonrpc: '2.0',
104
+ id,
105
+ result: {
106
+ content: [{
107
+ type: 'text',
108
+ text: JSON.stringify(result, null, 2)
109
+ }]
110
+ }
111
+ };
112
+ }
113
+
114
+ if (name === 'strest_template') {
115
+ const base = args.baseUrl || 'http://127.0.0.1:5000';
116
+ const template = {
117
+ name: "Generated Multi-Step Scenario",
118
+ steps: [
119
+ {
120
+ name: "auth_step",
121
+ url: `${base}/api/auth/login`,
122
+ method: "POST",
123
+ body: { username: "admin", password: "password123" },
124
+ assert: { status: 200, body: { token: "defined" } }
125
+ },
126
+ {
127
+ name: "query_step",
128
+ url: `${base}/api/data`,
129
+ method: "GET",
130
+ headers: { authorization: "Bearer <$ steps.auth_step.body.token $>" },
131
+ assert: { status: 200 }
132
+ }
133
+ ]
134
+ };
135
+ return {
136
+ jsonrpc: '2.0',
137
+ id,
138
+ result: {
139
+ content: [{
140
+ type: 'text',
141
+ text: JSON.stringify(template, null, 2)
142
+ }]
143
+ }
144
+ };
145
+ }
146
+
147
+ throw new Error(`Unknown tool: ${name}`);
148
+ } catch (err) {
149
+ return {
150
+ jsonrpc: '2.0',
151
+ id,
152
+ error: { code: -32000, message: err.message }
153
+ };
154
+ }
155
+ }
156
+
157
+ return {
158
+ jsonrpc: '2.0',
159
+ id,
160
+ error: { code: -32601, message: `Method not found: ${method}` }
161
+ };
162
+ }
163
+
164
+ if (process.argv[1] && process.argv[1].endsWith('server.mjs')) {
165
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
166
+
167
+ rl.on('line', async (line) => {
168
+ if (!line.trim()) return;
169
+ try {
170
+ const request = JSON.parse(line);
171
+ const response = await handleRequest(request);
172
+ if (response !== null && response !== undefined) {
173
+ process.stdout.write(JSON.stringify(response) + '\n');
174
+ }
175
+ } catch (err) {
176
+ process.stdout.write(JSON.stringify({
177
+ jsonrpc: '2.0',
178
+ id: null,
179
+ error: { code: -32700, message: 'Parse error: ' + err.message }
180
+ }) + '\n');
181
+ }
182
+ });
183
+ }