@ulexid/mcp-server 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # @ulexid/mcp-server
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
+ Version `0.1.1` — [Ulexid Documentation](https://docs.ulexid.com)
4
+
5
+ Stdio MCP proxy connecting AI coding agents to the hosted [Ulexid](https://www.ulexid.com) control plane (`https://ulexid.com/mcp`).
6
+
7
+ ```bash
8
+ npx @ulexid/mcp-server
9
+ ```
10
+
11
+ ## License
12
+
13
+ Proprietary (`UNLICENSED`). Copyright (c) Ulexid. All rights reserved.
@@ -0,0 +1,67 @@
1
+ export declare const ENVIRONMENT_ISSUERS: {
2
+ readonly prod: "https://ulexid.com";
3
+ readonly stage: "https://stage.ulexid.com";
4
+ readonly dev: "https://dev.ulexid.com";
5
+ };
6
+ export type UlexEnvironment = keyof typeof ENVIRONMENT_ISSUERS;
7
+ export interface ResolveEndpointOptions {
8
+ readonly url?: string;
9
+ readonly baseUrl?: string;
10
+ readonly env?: string;
11
+ }
12
+ export interface UlexMcpProxyClientOptions {
13
+ readonly endpoint: string;
14
+ readonly apiKey?: string;
15
+ readonly fetchFn?: typeof fetch;
16
+ }
17
+ export interface JsonRpcTransportResult {
18
+ readonly status: number;
19
+ readonly json?: unknown;
20
+ }
21
+ export interface StdioProxyOptions extends ResolveEndpointOptions {
22
+ readonly endpoint?: string;
23
+ readonly apiKey?: string;
24
+ readonly stdin?: NodeJS.ReadableStream;
25
+ readonly stdout?: NodeJS.WritableStream;
26
+ readonly stderr?: NodeJS.WritableStream;
27
+ readonly fetchFn?: typeof fetch;
28
+ }
29
+ export interface ProxyCliArgs {
30
+ readonly url?: string;
31
+ readonly env?: string;
32
+ readonly apiKey?: string;
33
+ readonly help: boolean;
34
+ }
35
+ /**
36
+ * Resolve the target hosted MCP endpoint URL from explicit flags or environment
37
+ * variables, defaulting to production (`https://ulexid.com/mcp`).
38
+ */
39
+ export declare function resolveMcpEndpoint(options?: ResolveEndpointOptions): string;
40
+ /**
41
+ * HTTP Streamable-MCP client that forwards JSON-RPC payloads to the hosted
42
+ * Ulexid control plane, maintains `Mcp-Session-Id`, and hot-captures the
43
+ * tenant `api_key` minted by `ulex_poll_agent_connection`.
44
+ */
45
+ export declare class UlexMcpProxyClient {
46
+ readonly endpoint: string;
47
+ private apiKey;
48
+ private sessionId;
49
+ private readonly fetchFn;
50
+ private nextRequestId;
51
+ constructor(options: UlexMcpProxyClientOptions);
52
+ getApiKey(): string | undefined;
53
+ setApiKey(apiKey: string | undefined): void;
54
+ getSessionId(): string | undefined;
55
+ sendJsonRpc(payload: unknown): Promise<JsonRpcTransportResult>;
56
+ initialize(clientName?: string, clientVersion?: string): Promise<unknown>;
57
+ listTools(): Promise<unknown[]>;
58
+ callTool(name: string, args?: Record<string, unknown>, explicitId?: string | number): Promise<unknown>;
59
+ }
60
+ export declare function parseProxyCliArgs(argv: readonly string[]): ProxyCliArgs;
61
+ export declare function formatProxyHelp(): string;
62
+ /**
63
+ * Run a newline-delimited JSON-RPC stdio loop forwarding every MCP message to
64
+ * the hosted Ulexid HTTP endpoint.
65
+ */
66
+ export declare function runStdioProxy(options?: StdioProxyOptions): Promise<void>;
67
+ export declare function runStdioProxyFromCli(argv: readonly string[]): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,394 @@
1
+ import { createInterface } from 'node:readline';
2
+ export const ENVIRONMENT_ISSUERS = {
3
+ prod: 'https://ulexid.com',
4
+ stage: 'https://stage.ulexid.com',
5
+ dev: 'https://dev.ulexid.com',
6
+ };
7
+ function isRecord(value) {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+ function normalizeEnvironment(raw) {
11
+ const normalized = raw.trim().toLowerCase();
12
+ if (normalized === 'prod' || normalized === 'production')
13
+ return 'prod';
14
+ if (normalized === 'stage' || normalized === 'staging')
15
+ return 'stage';
16
+ if (normalized === 'dev' || normalized === 'development')
17
+ return 'dev';
18
+ throw new Error(`Unknown ulex environment "${raw}". Expected one of: prod, stage, dev.`);
19
+ }
20
+ /**
21
+ * Resolve the target hosted MCP endpoint URL from explicit flags or environment
22
+ * variables, defaulting to production (`https://ulexid.com/mcp`).
23
+ */
24
+ export function resolveMcpEndpoint(options) {
25
+ const formatUrl = (rawUrl) => {
26
+ let parsed;
27
+ try {
28
+ parsed = new URL(rawUrl.trim());
29
+ }
30
+ catch {
31
+ throw new Error(`Invalid MCP URL: "${rawUrl}"`);
32
+ }
33
+ const cleanPath = parsed.pathname.replace(/\/+$/, '');
34
+ parsed.pathname = cleanPath.endsWith('/mcp') ? cleanPath : `${cleanPath}/mcp`;
35
+ return parsed.toString();
36
+ };
37
+ const explicitUrl = options?.url ?? options?.baseUrl;
38
+ if (explicitUrl !== undefined && explicitUrl.trim().length > 0) {
39
+ return formatUrl(explicitUrl);
40
+ }
41
+ if (options?.env !== undefined && options.env.trim().length > 0) {
42
+ return `${ENVIRONMENT_ISSUERS[normalizeEnvironment(options.env)]}/mcp`;
43
+ }
44
+ const envUrl = process.env['ULEX_MCP_URL'] ?? process.env['ULEX_URL'];
45
+ if (envUrl !== undefined && envUrl.trim().length > 0) {
46
+ return formatUrl(envUrl);
47
+ }
48
+ const rawEnv = process.env['ULEX_ENV'] ?? 'prod';
49
+ const envKey = normalizeEnvironment(rawEnv);
50
+ return `${ENVIRONMENT_ISSUERS[envKey]}/mcp`;
51
+ }
52
+ function extractApiKeyFromResponse(response) {
53
+ if (!isRecord(response))
54
+ return undefined;
55
+ const result = response['result'];
56
+ if (!isRecord(result) || result['isError'] === true)
57
+ return undefined;
58
+ const structured = result['structuredContent'];
59
+ if (isRecord(structured)) {
60
+ const data = isRecord(structured['data']) ? structured['data'] : structured;
61
+ const key = data['api_key'];
62
+ if (typeof key === 'string' && key.startsWith('ulex_')) {
63
+ return key;
64
+ }
65
+ }
66
+ const content = result['content'];
67
+ if (Array.isArray(content)) {
68
+ for (const item of content) {
69
+ if (!isRecord(item) || item['type'] !== 'text')
70
+ continue;
71
+ const text = item['text'];
72
+ if (typeof text !== 'string')
73
+ continue;
74
+ const trimmed = text.trim();
75
+ if (!trimmed.startsWith('{'))
76
+ continue;
77
+ try {
78
+ const parsed = JSON.parse(trimmed);
79
+ if (!isRecord(parsed))
80
+ continue;
81
+ const data = isRecord(parsed['data']) ? parsed['data'] : parsed;
82
+ const key = data['api_key'];
83
+ if (typeof key === 'string' && key.startsWith('ulex_')) {
84
+ return key;
85
+ }
86
+ }
87
+ catch {
88
+ // Non-JSON text blocks are expected in human-readable MCP tool output.
89
+ }
90
+ }
91
+ }
92
+ return undefined;
93
+ }
94
+ function extractToolCallName(payload) {
95
+ if (!isRecord(payload))
96
+ return undefined;
97
+ if (payload['method'] !== 'tools/call')
98
+ return undefined;
99
+ const params = payload['params'];
100
+ if (!isRecord(params))
101
+ return undefined;
102
+ const name = params['name'];
103
+ return typeof name === 'string' ? name : undefined;
104
+ }
105
+ function parseSseMessages(bodyText, expectedId) {
106
+ const frames = bodyText.split(/\r?\n\r?\n/);
107
+ const parsedMessages = [];
108
+ for (const frame of frames) {
109
+ const dataLines = [];
110
+ for (const line of frame.split(/\r?\n/)) {
111
+ if (line.startsWith('data:')) {
112
+ dataLines.push(line.slice(5).trimStart());
113
+ }
114
+ }
115
+ if (dataLines.length === 0)
116
+ continue;
117
+ const raw = dataLines.join('\n').trim();
118
+ if (raw.length === 0 || raw === '[DONE]')
119
+ continue;
120
+ try {
121
+ parsedMessages.push(JSON.parse(raw));
122
+ }
123
+ catch {
124
+ // Skip malformed partial SSE keep-alive/comment frames.
125
+ }
126
+ }
127
+ if (parsedMessages.length === 0) {
128
+ throw new Error('Hosted MCP SSE response contained no JSON-RPC payload.');
129
+ }
130
+ if (expectedId !== undefined) {
131
+ for (const msg of parsedMessages) {
132
+ if (isRecord(msg) && msg['id'] === expectedId) {
133
+ return msg;
134
+ }
135
+ }
136
+ }
137
+ return parsedMessages[parsedMessages.length - 1];
138
+ }
139
+ /**
140
+ * HTTP Streamable-MCP client that forwards JSON-RPC payloads to the hosted
141
+ * Ulexid control plane, maintains `Mcp-Session-Id`, and hot-captures the
142
+ * tenant `api_key` minted by `ulex_poll_agent_connection`.
143
+ */
144
+ export class UlexMcpProxyClient {
145
+ endpoint;
146
+ apiKey;
147
+ sessionId;
148
+ fetchFn;
149
+ nextRequestId = 1;
150
+ constructor(options) {
151
+ this.endpoint = options.endpoint;
152
+ const initialKey = options.apiKey ?? process.env['ULEX_API_KEY'];
153
+ this.apiKey = initialKey && initialKey.trim().length > 0 ? initialKey.trim() : undefined;
154
+ this.fetchFn = options.fetchFn ?? fetch;
155
+ }
156
+ getApiKey() {
157
+ return this.apiKey;
158
+ }
159
+ setApiKey(apiKey) {
160
+ this.apiKey = apiKey && apiKey.trim().length > 0 ? apiKey.trim() : undefined;
161
+ }
162
+ getSessionId() {
163
+ return this.sessionId;
164
+ }
165
+ async sendJsonRpc(payload) {
166
+ const headers = {
167
+ 'Content-Type': 'application/json',
168
+ Accept: 'application/json, text/event-stream',
169
+ };
170
+ if (this.apiKey !== undefined) {
171
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
172
+ }
173
+ if (this.sessionId !== undefined) {
174
+ headers['Mcp-Session-Id'] = this.sessionId;
175
+ }
176
+ const res = await this.fetchFn(this.endpoint, {
177
+ method: 'POST',
178
+ headers,
179
+ body: JSON.stringify(payload),
180
+ });
181
+ const returnedSession = res.headers.get('mcp-session-id');
182
+ if (returnedSession !== null && returnedSession.trim().length > 0) {
183
+ this.sessionId = returnedSession.trim();
184
+ }
185
+ if (res.status === 202) {
186
+ return { status: 202 };
187
+ }
188
+ const bodyText = await res.text();
189
+ const contentType = (res.headers.get('content-type') ?? '').toLowerCase();
190
+ if (!res.ok) {
191
+ if (contentType.includes('application/json') && bodyText.trim().length > 0) {
192
+ try {
193
+ const parsedError = JSON.parse(bodyText);
194
+ if (isRecord(parsedError) && ('jsonrpc' in parsedError || 'error' in parsedError)) {
195
+ return { status: res.status, json: parsedError };
196
+ }
197
+ }
198
+ catch {
199
+ // Fall through to status error below.
200
+ }
201
+ }
202
+ throw new Error(`Hosted MCP request failed with HTTP ${String(res.status)}: ${bodyText.slice(0, 300)}`);
203
+ }
204
+ if (bodyText.trim().length === 0) {
205
+ return { status: res.status };
206
+ }
207
+ const requestId = isRecord(payload) ? payload['id'] : undefined;
208
+ const json = contentType.includes('text/event-stream')
209
+ ? parseSseMessages(bodyText, requestId)
210
+ : JSON.parse(bodyText);
211
+ const toolName = extractToolCallName(payload);
212
+ if (toolName !== undefined && (toolName === 'ulex_poll_agent_connection' || this.apiKey === undefined)) {
213
+ const captured = extractApiKeyFromResponse(json);
214
+ if (captured !== undefined) {
215
+ this.apiKey = captured;
216
+ }
217
+ }
218
+ return { status: res.status, json };
219
+ }
220
+ async initialize(clientName = '@ulexid/mcp-server-proxy', clientVersion = '0.1.1') {
221
+ const id = this.nextRequestId++;
222
+ const { json } = await this.sendJsonRpc({
223
+ jsonrpc: '2.0',
224
+ id,
225
+ method: 'initialize',
226
+ params: {
227
+ protocolVersion: '2024-11-05',
228
+ capabilities: {},
229
+ clientInfo: { name: clientName, version: clientVersion },
230
+ },
231
+ });
232
+ await this.sendJsonRpc({
233
+ jsonrpc: '2.0',
234
+ method: 'notifications/initialized',
235
+ params: {},
236
+ });
237
+ return json;
238
+ }
239
+ async listTools() {
240
+ if (this.sessionId === undefined) {
241
+ await this.initialize();
242
+ }
243
+ const id = this.nextRequestId++;
244
+ const { json } = await this.sendJsonRpc({
245
+ jsonrpc: '2.0',
246
+ id,
247
+ method: 'tools/list',
248
+ params: {},
249
+ });
250
+ if (isRecord(json) && isRecord(json['error'])) {
251
+ const msg = typeof json['error']['message'] === 'string' ? json['error']['message'] : 'MCP tools/list error';
252
+ throw new Error(msg);
253
+ }
254
+ if (isRecord(json) && isRecord(json['result']) && Array.isArray(json['result']['tools'])) {
255
+ return json['result']['tools'];
256
+ }
257
+ return [];
258
+ }
259
+ async callTool(name, args = {}, explicitId) {
260
+ if (this.sessionId === undefined) {
261
+ await this.initialize();
262
+ }
263
+ const id = explicitId ?? this.nextRequestId++;
264
+ const { json } = await this.sendJsonRpc({
265
+ jsonrpc: '2.0',
266
+ id,
267
+ method: 'tools/call',
268
+ params: { name, arguments: args },
269
+ });
270
+ if (isRecord(json) && isRecord(json['error'])) {
271
+ const msg = typeof json['error']['message'] === 'string' ? json['error']['message'] : `MCP error calling ${name}`;
272
+ throw new Error(msg);
273
+ }
274
+ if (isRecord(json) && 'result' in json) {
275
+ return json['result'];
276
+ }
277
+ return json;
278
+ }
279
+ }
280
+ export function parseProxyCliArgs(argv) {
281
+ let url;
282
+ let env;
283
+ let apiKey;
284
+ let help = false;
285
+ for (let i = 0; i < argv.length; i++) {
286
+ const token = argv[i];
287
+ if (token === undefined)
288
+ continue;
289
+ if (token === '-h' || token === '--help') {
290
+ help = true;
291
+ }
292
+ else if (token === '--url' || token.startsWith('--url=')) {
293
+ url = token.includes('=') ? token.slice(token.indexOf('=') + 1) : argv[++i];
294
+ }
295
+ else if (token === '--env' || token.startsWith('--env=')) {
296
+ env = token.includes('=') ? token.slice(token.indexOf('=') + 1) : argv[++i];
297
+ }
298
+ else if (token === '--api-key' || token.startsWith('--api-key=')) {
299
+ apiKey = token.includes('=') ? token.slice(token.indexOf('=') + 1) : argv[++i];
300
+ }
301
+ }
302
+ return {
303
+ ...(url !== undefined ? { url } : {}),
304
+ ...(env !== undefined ? { env } : {}),
305
+ ...(apiKey !== undefined ? { apiKey } : {}),
306
+ help,
307
+ };
308
+ }
309
+ export function formatProxyHelp() {
310
+ return [
311
+ '@ulexid/mcp-server — stdio proxy for the hosted Ulexid MCP control plane',
312
+ '',
313
+ 'Usage:',
314
+ ' npx @ulexid/mcp-server [options]',
315
+ '',
316
+ 'Options:',
317
+ ' --env <prod|stage|dev> Target Ulexid environment (default: prod -> https://ulexid.com/mcp)',
318
+ ' --url <url> Override full Ulexid base or /mcp endpoint URL',
319
+ ' --api-key <key> Tenant API key (defaults to ULEX_API_KEY env var)',
320
+ ' -h, --help Show this help message',
321
+ '',
322
+ 'Environment variables:',
323
+ ' ULEX_MCP_URL / ULEX_URL Explicit hosted endpoint URL override',
324
+ ' ULEX_ENV Environment selector (prod, stage, dev)',
325
+ ' ULEX_API_KEY Bearer API key sent to the hosted control plane',
326
+ ].join('\n');
327
+ }
328
+ /**
329
+ * Run a newline-delimited JSON-RPC stdio loop forwarding every MCP message to
330
+ * the hosted Ulexid HTTP endpoint.
331
+ */
332
+ export async function runStdioProxy(options = {}) {
333
+ const endpoint = options.endpoint ?? resolveMcpEndpoint(options);
334
+ const client = new UlexMcpProxyClient({
335
+ endpoint,
336
+ ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
337
+ ...(options.fetchFn !== undefined ? { fetchFn: options.fetchFn } : {}),
338
+ });
339
+ const stdin = options.stdin ?? process.stdin;
340
+ const stdout = options.stdout ?? process.stdout;
341
+ const stderr = options.stderr ?? process.stderr;
342
+ const rl = createInterface({ input: stdin, crlfDelay: Infinity });
343
+ let chain = Promise.resolve();
344
+ const processLine = async (line) => {
345
+ const trimmed = line.trim();
346
+ if (trimmed.length === 0)
347
+ return;
348
+ let payload;
349
+ try {
350
+ payload = JSON.parse(trimmed);
351
+ }
352
+ catch {
353
+ stdout.write(`${JSON.stringify({
354
+ jsonrpc: '2.0',
355
+ id: null,
356
+ error: { code: -32700, message: 'Parse error: invalid JSON on stdin' },
357
+ })}\n`);
358
+ return;
359
+ }
360
+ const requestId = isRecord(payload) && 'id' in payload ? payload['id'] : undefined;
361
+ try {
362
+ const response = await client.sendJsonRpc(payload);
363
+ if (response.json !== undefined) {
364
+ stdout.write(`${JSON.stringify(response.json)}\n`);
365
+ }
366
+ }
367
+ catch (err) {
368
+ const message = err instanceof Error ? err.message : String(err);
369
+ stderr.write(`[ulexid-mcp] ${message}\n`);
370
+ if (requestId !== undefined) {
371
+ stdout.write(`${JSON.stringify({
372
+ jsonrpc: '2.0',
373
+ id: requestId,
374
+ error: { code: -32603, message },
375
+ })}\n`);
376
+ }
377
+ }
378
+ };
379
+ rl.on('line', (line) => {
380
+ chain = chain.then(() => processLine(line));
381
+ });
382
+ await new Promise((resolve) => {
383
+ rl.once('close', resolve);
384
+ });
385
+ await chain;
386
+ }
387
+ export async function runStdioProxyFromCli(argv) {
388
+ const args = parseProxyCliArgs(argv);
389
+ if (args.help) {
390
+ process.stdout.write(`${formatProxyHelp()}\n`);
391
+ return;
392
+ }
393
+ await runStdioProxy(args);
394
+ }
package/dist/stdio.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { runStdioProxyFromCli } from './index.js';
3
+ runStdioProxyFromCli(process.argv.slice(2)).catch((err) => {
4
+ process.stderr.write(`[ulexid-mcp] ${err instanceof Error ? err.message : String(err)}\n`);
5
+ process.exit(1);
6
+ });
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@ulexid/mcp-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "license": "UNLICENSED",
5
+ "type": "module",
6
+ "description": "Ulexid MCP stdio proxy — connect AI coding agents to https://ulexid.com/mcp",
7
+ "homepage": "https://www.ulexid.com",
8
+ "engines": {
9
+ "node": ">=22.0.0"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "bin": {
17
+ "mcp-server": "dist/stdio.js",
18
+ "ulex-mcp": "dist/stdio.js",
19
+ "ulexid-mcp": "dist/stdio.js"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./stdio": "./dist/stdio.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "dependencies": {}
33
+ }