@intelli-1113/stratos-sdk 1.0.0 → 1.2.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/README.md +19 -0
- package/package.json +3 -2
- package/src/mcp-proxy.js +112 -0
package/README.md
CHANGED
|
@@ -37,3 +37,22 @@ OpenLLMetry patches `openai`/`langchain` to capture spans — it has to run **be
|
|
|
37
37
|
|
|
38
38
|
## Changing the URL or token
|
|
39
39
|
Both come from env, read at startup — change the `.env` value and restart. No code edit, no SDK re-publish.
|
|
40
|
+
|
|
41
|
+
## MCP servers (Claude Desktop / Cursor / VS Code)
|
|
42
|
+
The same package ships a `stratos-mcp-proxy` command for monitoring host-launched MCP servers.
|
|
43
|
+
Install it globally, add an agent (type `mcp`) in Stratos, then wrap the server's command:
|
|
44
|
+
```bash
|
|
45
|
+
npm i -g @intelli-1113/stratos-sdk
|
|
46
|
+
```
|
|
47
|
+
```json
|
|
48
|
+
"weather": {
|
|
49
|
+
"command": "stratos-mcp-proxy",
|
|
50
|
+
"args": ["--","npx","-y","@scope/weather-mcp@latest"],
|
|
51
|
+
"env": {
|
|
52
|
+
"STRATOS_TOKEN":"<token>",
|
|
53
|
+
"STRATOS_URL":"http://localhost:4000",
|
|
54
|
+
"STRATOS_APP_NAME":"weather" }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
It forwards the JSON-RPC stream verbatim and reports each **tool call** (name, args, result, latency) + liveness.
|
|
58
|
+
Note: host MCP servers don't call the LLM themselves, so tokens/cost aren't captured.
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intelli-1113/stratos-sdk",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "One
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "One install for Stratos AI: OpenLLMetry agent telemetry (--import) AND the stratos-mcp-proxy for MCP servers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js",
|
|
9
9
|
"./register": "./src/register.js"
|
|
10
10
|
},
|
|
11
|
+
"bin": { "stratos-mcp-proxy": "./src/mcp-proxy.js" },
|
|
11
12
|
"files": ["src"],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"test": "node -e \"import('./src/index.js').then(m=>console.log('ok',Object.keys(m)))\""
|
package/src/mcp-proxy.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* stratos-mcp-proxy (bundled in @intelli-1113/stratos-sdk) — transparent MCP
|
|
4
|
+
* stdio proxy that meters tool calls for host-launched MCP servers
|
|
5
|
+
* (Claude Desktop / Cursor / VS Code / Claude Code).
|
|
6
|
+
*
|
|
7
|
+
* It spawns the real server, forwards the JSON-RPC stream verbatim in both
|
|
8
|
+
* directions, and reports each tools/call to Stratos as an OTLP/JSON span in
|
|
9
|
+
* OpenLLMetry's shape (so /api/ingest treats it like a real traceloop tool span).
|
|
10
|
+
* stdout is reserved for the protocol; all logging goes to stderr.
|
|
11
|
+
*
|
|
12
|
+
* Host config:
|
|
13
|
+
* "weather": {
|
|
14
|
+
* "command": "stratos-mcp-proxy",
|
|
15
|
+
* "args": ["--","npx","-y","@scope/weather-mcp@latest"],
|
|
16
|
+
* "env": { "STRATOS_TOKEN":"<token>", "STRATOS_URL":"http://localhost:4000",
|
|
17
|
+
* "STRATOS_APP_NAME":"weather" }
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// CRITICAL: stdout is the MCP channel — keep it clean. Route all logging to stderr.
|
|
22
|
+
console.log = console.info = (...a) => process.stderr.write(a.map(String).join(" ") + "\n");
|
|
23
|
+
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import { randomBytes } from "node:crypto";
|
|
26
|
+
|
|
27
|
+
let argv = process.argv.slice(2);
|
|
28
|
+
const sep = argv.indexOf("--");
|
|
29
|
+
if (sep !== -1) argv = argv.slice(sep + 1);
|
|
30
|
+
if (!argv.length) { process.stderr.write("[stratos-proxy] usage: stratos-mcp-proxy [--] <command> [args...]\n"); process.exit(2); }
|
|
31
|
+
const [cmd, ...args] = argv;
|
|
32
|
+
const target = [cmd, ...args].join(" ");
|
|
33
|
+
|
|
34
|
+
const TOKEN = process.env.STRATOS_TOKEN || "";
|
|
35
|
+
const URL = (process.env.STRATOS_URL || "http://localhost:4000").replace(/\/+$/, "");
|
|
36
|
+
const APP_NAME = process.env.STRATOS_APP_NAME || ("mcp:" + (args[args.length - 1] || cmd).replace(/[^a-zA-Z0-9._@/-]/g, "").slice(-40));
|
|
37
|
+
const HEARTBEAT_MS = Number(process.env.STRATOS_HEARTBEAT_MS || 30000);
|
|
38
|
+
|
|
39
|
+
if (!TOKEN) process.stderr.write("[stratos-proxy] STRATOS_TOKEN not set — telemetry disabled (proxying only).\n");
|
|
40
|
+
console.log(`[stratos-proxy] proxying "${target}" as "${APP_NAME}" → ${URL}`);
|
|
41
|
+
|
|
42
|
+
const hdr = { "Content-Type": "application/json", "x-stratos-token": TOKEN };
|
|
43
|
+
const hex = (n) => randomBytes(n).toString("hex");
|
|
44
|
+
const nano = (ms) => (BigInt(Math.round(ms)) * 1000000n).toString();
|
|
45
|
+
|
|
46
|
+
function reportTool(name, argsObj, out, isErr, startMs, endMs) {
|
|
47
|
+
if (!TOKEN) return;
|
|
48
|
+
const attr = (k, v) => ({ key: k, value: { stringValue: String(v) } });
|
|
49
|
+
const body = {
|
|
50
|
+
resourceSpans: [{
|
|
51
|
+
resource: { attributes: [attr("service.name", APP_NAME)] },
|
|
52
|
+
scopeSpans: [{ spans: [{
|
|
53
|
+
traceId: hex(16), spanId: hex(8),
|
|
54
|
+
name: `${name}.tool`,
|
|
55
|
+
startTimeUnixNano: nano(startMs), endTimeUnixNano: nano(endMs),
|
|
56
|
+
attributes: [
|
|
57
|
+
attr("traceloop.span.kind", "tool"),
|
|
58
|
+
attr("traceloop.entity.name", name),
|
|
59
|
+
attr("traceloop.entity.input", JSON.stringify({ tool_name: name, arguments: argsObj || {} })),
|
|
60
|
+
attr("traceloop.entity.output", JSON.stringify({ content: out, is_error: !!isErr })),
|
|
61
|
+
],
|
|
62
|
+
}] }],
|
|
63
|
+
}],
|
|
64
|
+
};
|
|
65
|
+
fetch(`${URL}/api/ingest`, { method: "POST", headers: hdr, body: JSON.stringify(body) }).catch(() => {});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function heartbeat() {
|
|
69
|
+
if (!TOKEN) return;
|
|
70
|
+
fetch(`${URL}/api/heartbeat`, { method: "POST", headers: hdr }).catch(() => {});
|
|
71
|
+
}
|
|
72
|
+
heartbeat();
|
|
73
|
+
const hbTimer = setInterval(heartbeat, HEARTBEAT_MS);
|
|
74
|
+
hbTimer.unref?.();
|
|
75
|
+
|
|
76
|
+
const pendingCalls = new Map();
|
|
77
|
+
|
|
78
|
+
const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"], shell: process.platform === "win32" });
|
|
79
|
+
child.on("error", (e) => { process.stderr.write(`[stratos-proxy] failed to start target: ${e.message}\n`); process.exit(1); });
|
|
80
|
+
child.on("exit", (code) => { try { heartbeat(); clearInterval(hbTimer); } catch {} process.exit(code == null ? 0 : code); });
|
|
81
|
+
|
|
82
|
+
linePump(process.stdin, (line) => {
|
|
83
|
+
let m; try { m = JSON.parse(line); } catch { child.stdin.write(line + "\n"); return; }
|
|
84
|
+
if (m && m.id != null && m.method === "tools/call") {
|
|
85
|
+
const name = (m.params && m.params.name) || "tool";
|
|
86
|
+
pendingCalls.set(m.id, { name, args: (m.params && m.params.arguments) || {}, start: Date.now() });
|
|
87
|
+
}
|
|
88
|
+
child.stdin.write(line + "\n");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
linePump(child.stdout, (line) => {
|
|
92
|
+
process.stdout.write(line + "\n");
|
|
93
|
+
let m; try { m = JSON.parse(line); } catch { return; }
|
|
94
|
+
if (!m || m.id == null || !pendingCalls.has(m.id)) return;
|
|
95
|
+
const p = pendingCalls.get(m.id); pendingCalls.delete(m.id);
|
|
96
|
+
const isErr = m.error != null;
|
|
97
|
+
const out = isErr ? (m.error.message || "error") : (m.result != null ? m.result : {});
|
|
98
|
+
reportTool(p.name, p.args, out, isErr, p.start, Date.now());
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function linePump(stream, onLine) {
|
|
102
|
+
let buf = "";
|
|
103
|
+
stream.on("data", (chunk) => {
|
|
104
|
+
buf += chunk.toString("utf8");
|
|
105
|
+
let i;
|
|
106
|
+
while ((i = buf.indexOf("\n")) !== -1) {
|
|
107
|
+
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
108
|
+
if (line.trim()) onLine(line);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
stream.on("end", () => { if (buf.trim()) onLine(buf); });
|
|
112
|
+
}
|