unreal-mcp-proxy 0.1.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.
@@ -0,0 +1,137 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createInterface } from "node:readline";
5
+ /**
6
+ * stdio mode (`unreal-mcp-proxy --stdio`): lets an MCP client auto-start everything.
7
+ * Registered as a stdio server in the client config, this thin shim
8
+ * 1. ensures the HTTP proxy daemon is running (spawns it detached if the port is closed),
9
+ * 2. bridges newline-delimited JSON-RPC on stdio to the daemon's /mcp endpoint.
10
+ * The daemon owns recording and the viewer, and outlives the client session.
11
+ * All shim logging goes to stderr - stdout is the protocol channel.
12
+ */
13
+ const log = (message) => void process.stderr.write(`[unreal-mcp-proxy] ${message}\n`);
14
+ async function healthy(baseUrl) {
15
+ try {
16
+ const response = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(500) });
17
+ return response.ok;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ export async function ensureDaemon(config) {
24
+ const baseUrl = `http://127.0.0.1:${config.listenPort}`;
25
+ if (await healthy(baseUrl))
26
+ return baseUrl;
27
+ const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "cli.js");
28
+ log(`starting proxy daemon on port ${config.listenPort}`);
29
+ spawn(process.execPath, [cliPath], {
30
+ detached: true,
31
+ stdio: "ignore",
32
+ env: process.env,
33
+ windowsHide: true
34
+ }).unref();
35
+ for (let attempt = 0; attempt < 25; attempt += 1) {
36
+ await new Promise((resolve) => setTimeout(resolve, 200));
37
+ if (await healthy(baseUrl)) {
38
+ log(`daemon ready: ${baseUrl}`);
39
+ return baseUrl;
40
+ }
41
+ }
42
+ throw new Error(`proxy daemon did not become healthy on ${baseUrl}`);
43
+ }
44
+ export async function runStdioBridge(config) {
45
+ const baseUrl = await ensureDaemon(config);
46
+ const endpoint = `${baseUrl}/mcp`;
47
+ let mcpSessionId = null;
48
+ let notificationStreamStarted = false;
49
+ const writeOut = (payload) => void process.stdout.write(`${JSON.stringify(payload)}\n`);
50
+ /** Server→client notifications (e.g. tools/listChanged) arrive on a GET SSE stream. */
51
+ const startNotificationStream = async () => {
52
+ if (notificationStreamStarted || !mcpSessionId)
53
+ return;
54
+ notificationStreamStarted = true;
55
+ try {
56
+ const response = await fetch(endpoint, {
57
+ method: "GET",
58
+ headers: { accept: "text/event-stream", "mcp-session-id": mcpSessionId }
59
+ });
60
+ if (!response.ok || !response.body)
61
+ return;
62
+ let pending = "";
63
+ const decoder = new TextDecoder();
64
+ for await (const chunk of response.body) {
65
+ pending += decoder.decode(chunk, { stream: true });
66
+ const blocks = pending.split(/\r?\n\r?\n/);
67
+ pending = blocks.pop() ?? "";
68
+ for (const block of blocks)
69
+ emitSseBlock(block, writeOut);
70
+ }
71
+ }
72
+ catch { /* optional channel - the editor may not support a standalone GET stream */ }
73
+ };
74
+ const rl = createInterface({ input: process.stdin, terminal: false });
75
+ for await (const line of rl) {
76
+ const trimmed = line.trim();
77
+ if (!trimmed)
78
+ continue;
79
+ let message;
80
+ try {
81
+ message = JSON.parse(trimmed);
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ try {
87
+ const response = await fetch(endpoint, {
88
+ method: "POST",
89
+ headers: {
90
+ "content-type": "application/json",
91
+ accept: "application/json, text/event-stream",
92
+ ...(mcpSessionId ? { "mcp-session-id": mcpSessionId } : {})
93
+ },
94
+ body: trimmed
95
+ });
96
+ const sessionHeader = response.headers.get("mcp-session-id");
97
+ if (sessionHeader)
98
+ mcpSessionId = sessionHeader;
99
+ if (message.method === "initialize")
100
+ void startNotificationStream();
101
+ if (message.id === undefined) {
102
+ void response.arrayBuffer();
103
+ continue;
104
+ } // notification: no reply
105
+ const contentType = response.headers.get("content-type") ?? "";
106
+ const text = await response.text();
107
+ if (contentType.includes("text/event-stream")) {
108
+ for (const block of text.split(/\r?\n\r?\n/))
109
+ emitSseBlock(block, writeOut);
110
+ }
111
+ else if (text.trim()) {
112
+ writeOut(JSON.parse(text));
113
+ }
114
+ else {
115
+ writeOut({ jsonrpc: "2.0", id: message.id, error: { code: -32603, message: `empty response (HTTP ${response.status})` } });
116
+ }
117
+ }
118
+ catch (error) {
119
+ if (message.id !== undefined) {
120
+ writeOut({ jsonrpc: "2.0", id: message.id, error: { code: -32603, message: error.message } });
121
+ }
122
+ }
123
+ }
124
+ }
125
+ function emitSseBlock(block, writeOut) {
126
+ const data = block.split(/\r?\n/)
127
+ .filter((line) => line.startsWith("data:"))
128
+ .map((line) => line.slice(5).trimStart())
129
+ .join("\n");
130
+ if (!data)
131
+ return;
132
+ try {
133
+ writeOut(JSON.parse(data));
134
+ }
135
+ catch { /* keep-alive or partial frame */ }
136
+ }
137
+ //# sourceMappingURL=stdio-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio-bridge.js","sourceRoot":"","sources":["../src/stdio-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /** Shared domain types for the proxy core, the viewer, and tests. */
2
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export type JsonObject = Record<string, JsonValue | undefined>;
6
+ /** One recorded event line in a session .jsonl file. */
7
+ export interface SessionEvent {
8
+ schemaVersion: 2;
9
+ source: string;
10
+ sessionId: string;
11
+ sequence: number;
12
+ timestamp: string;
13
+ type: string;
14
+ /** Optional metadata tag (user/machine/project); null unless configured. */
15
+ identity: JsonObject | null;
16
+ [key: string]: unknown;
17
+ }
18
+ export interface ActiveSession {
19
+ id: string;
20
+ createdAt: string;
21
+ reason: string;
22
+ sequence: number;
23
+ }
24
+ /** Body summary produced by summarizeBody(): either parsed JSON, an SSE bundle, or an omission marker. */
25
+ export type BodySummary = {
26
+ omitted: true;
27
+ reason: string;
28
+ size?: number;
29
+ } | {
30
+ transport: "sse";
31
+ events: JsonValue[];
32
+ } | JsonValue;
33
+ export type CallOutcome = "success" | "error" | "running";
34
+ export interface CallModel {
35
+ id: string;
36
+ sequence: number;
37
+ title: string;
38
+ subtitle?: string;
39
+ category: string;
40
+ toolName?: string;
41
+ toolsetName?: string;
42
+ arguments?: JsonObject;
43
+ startedAt: string;
44
+ completedAt: string | null;
45
+ durationMs: number | null;
46
+ statusCode: number | null;
47
+ outcome: CallOutcome;
48
+ error: string | null;
49
+ request: BodySummary | null;
50
+ response: BodySummary | null;
51
+ requestHeaders: JsonObject | null;
52
+ responseHeaders: JsonObject | null;
53
+ timing: CallTiming | null;
54
+ clientSource: string;
55
+ operationId?: string;
56
+ operationTitle?: string;
57
+ operationStep?: string;
58
+ deepLink: string;
59
+ annotations: AnnotationModel[];
60
+ isSystem: boolean;
61
+ performance?: CallPerformance;
62
+ }
63
+ export interface CallTiming {
64
+ requestToHeadersMs: number;
65
+ toolResultMs: number;
66
+ proxyFinalizeMs: number;
67
+ completionReason: string;
68
+ }
69
+ export interface CallPerformance {
70
+ sampleCount: number;
71
+ medianMs: number;
72
+ ratio: number;
73
+ classification: "slow" | "normal";
74
+ }
75
+ export interface AnnotationModel {
76
+ callId: string;
77
+ severity: string;
78
+ title: string;
79
+ summary: string;
80
+ cause?: string;
81
+ suggestion?: string;
82
+ author: string;
83
+ timestamp?: string;
84
+ }
85
+ export interface FlowNode {
86
+ id: string;
87
+ key: string;
88
+ label: string;
89
+ subtitle: string;
90
+ kind: "tool" | "target";
91
+ callIds: string[];
92
+ }
93
+ export interface FlowEdge {
94
+ id: string;
95
+ from: string;
96
+ to: string;
97
+ order: number;
98
+ callId: string;
99
+ durationMs: number;
100
+ outcome: CallOutcome;
101
+ }
102
+ export interface FlowGraph {
103
+ basis: string;
104
+ nodes: FlowNode[];
105
+ edges: FlowEdge[];
106
+ }
107
+ export interface SessionModel {
108
+ id: string;
109
+ startedAt: string | undefined;
110
+ lastEventAt: string | undefined;
111
+ summary: {
112
+ totalCalls: number;
113
+ successes: number;
114
+ errors: number;
115
+ running: number;
116
+ systemCalls: number;
117
+ averageDurationMs: number;
118
+ medianDurationMs: number;
119
+ };
120
+ graph: FlowGraph;
121
+ calls: CallModel[];
122
+ annotations: AnnotationModel[];
123
+ rawEventCount: number;
124
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ /** Shared domain types for the proxy core, the viewer, and tests. */
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qEAAqE"}