@ophirai/mcp-server 0.2.0 → 0.2.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 ADDED
@@ -0,0 +1,169 @@
1
+ # @ophirai/mcp-server
2
+
3
+ MCP server that gives your AI agent the ability to discover, negotiate with, and manage AI service providers through the Ophir protocol.
4
+
5
+ No API keys required — connects to the public Ophir registry.
6
+
7
+ ## Installation
8
+
9
+ ### Claude Desktop
10
+
11
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "ophir": {
17
+ "command": "npx",
18
+ "args": ["@ophirai/mcp-server"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ ### Cursor
25
+
26
+ Add to `.cursor/mcp.json` in your project root:
27
+
28
+ ```json
29
+ {
30
+ "ophir": {
31
+ "command": "npx",
32
+ "args": ["@ophirai/mcp-server"]
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Windsurf
38
+
39
+ Add to your Windsurf MCP config:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "ophir": {
45
+ "command": "npx",
46
+ "args": ["@ophirai/mcp-server"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### Cline
53
+
54
+ Add to your Cline MCP settings:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "ophir": {
60
+ "command": "npx",
61
+ "args": ["@ophirai/mcp-server"]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Any MCP client (npx)
68
+
69
+ ```bash
70
+ npx @ophirai/mcp-server
71
+ ```
72
+
73
+ ### Smithery
74
+
75
+ ```bash
76
+ smithery install ophir-negotiate
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ | Environment Variable | Description | Default |
82
+ |---|---|---|
83
+ | `OPHIR_REGISTRY_URL` | Ophir registry endpoint | `https://registry.ophir.ai/v1` |
84
+ | `OPHIR_SELLERS` | Comma-separated seller endpoints | Auto-discovered |
85
+ | `OPHIR_BUYER_ENDPOINT` | Local buyer agent endpoint | `http://localhost:3001` |
86
+
87
+ ## What can it do?
88
+
89
+ ### ophir_discover — Find providers
90
+
91
+ > "Find me inference providers with a reputation score above 80."
92
+
93
+ Returns a list of providers with their services, pricing, and reputation scores:
94
+
95
+ ```json
96
+ {
97
+ "providers": [
98
+ {
99
+ "agent_id": "provider-abc",
100
+ "endpoint": "https://provider-abc.ophir.ai",
101
+ "services": [{ "category": "inference", "base_price": "0.002", "currency": "USDC", "unit": "request" }],
102
+ "reputation": { "score": 92, "total_agreements": 1847 }
103
+ }
104
+ ],
105
+ "total": 3
106
+ }
107
+ ```
108
+
109
+ ### ophir_negotiate — Full negotiation flow
110
+
111
+ > "Negotiate inference service with a max budget of 0.005 USDC per request."
112
+
113
+ Sends an RFQ to all matching providers, collects quotes, ranks them, and returns the best option:
114
+
115
+ ```json
116
+ {
117
+ "best_quote": {
118
+ "seller": "provider-abc",
119
+ "price": "0.0018",
120
+ "currency": "USDC",
121
+ "unit": "request",
122
+ "sla": [
123
+ { "metric": "latency_p99", "target": "500ms" },
124
+ { "metric": "uptime", "target": "99.9%" }
125
+ ]
126
+ },
127
+ "total_quotes": 5,
128
+ "rfq_id": "rfq_01HXYZ..."
129
+ }
130
+ ```
131
+
132
+ ### ophir_accept_quote — Accept a quote
133
+
134
+ > "Accept quote qt_abc123 from RFQ rfq_01HXYZ."
135
+
136
+ Creates a signed agreement with the selected provider, locking in the negotiated terms.
137
+
138
+ ### ophir_check_agreement — Monitor SLA compliance
139
+
140
+ > "Check if agreement agr_xyz is meeting its SLA targets."
141
+
142
+ Returns current SLA metric observations and compliance status:
143
+
144
+ ```json
145
+ {
146
+ "agreement_id": "agr_xyz",
147
+ "sla_metrics": [
148
+ { "name": "latency_p99", "target": "500ms", "comparison": "lte" },
149
+ { "name": "uptime", "target": "99.9%", "comparison": "gte" }
150
+ ],
151
+ "compliance_status": "monitoring_available"
152
+ }
153
+ ```
154
+
155
+ ### ophir_list_agreements — View active agreements
156
+
157
+ > "Show me all my active provider agreements."
158
+
159
+ Lists all agreements with their pricing, status, and SLA terms.
160
+
161
+ ### ophir_dispute — File an SLA dispute
162
+
163
+ > "File a dispute against agreement agr_xyz for latency violations."
164
+
165
+ Submits a dispute with cryptographic evidence of SLA violations, triggering the protocol's dispute resolution process.
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,424 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { OphirMCPServer, handleNegotiateService, handleHealth, } from '../index.js';
3
+ import { VERSION } from '../version.js';
4
+ import { StdioTransport } from '../stdio.js';
5
+ // Mock @ophirai/sdk to avoid real network calls
6
+ const mockRequestQuotes = vi.fn();
7
+ const mockWaitForQuotes = vi.fn();
8
+ const mockRankQuotes = vi.fn();
9
+ const mockClose = vi.fn();
10
+ const mockAutoDiscover = vi.fn();
11
+ vi.mock('@ophirai/sdk', () => ({
12
+ BuyerAgent: vi.fn().mockImplementation(() => ({
13
+ requestQuotes: mockRequestQuotes,
14
+ waitForQuotes: mockWaitForQuotes,
15
+ rankQuotes: mockRankQuotes,
16
+ close: mockClose,
17
+ })),
18
+ autoDiscover: (...args) => mockAutoDiscover(...args),
19
+ OphirRegistry: vi.fn().mockImplementation(() => ({
20
+ find: vi.fn().mockResolvedValue([]),
21
+ })),
22
+ negotiate: vi.fn(),
23
+ }));
24
+ // ── Protocol Tests ──────────────────────────────────────────────────
25
+ describe('MCP Protocol', () => {
26
+ let server;
27
+ beforeEach(() => {
28
+ server = new OphirMCPServer({ sellers: [] });
29
+ });
30
+ it('initialize returns correct protocol version and server info', async () => {
31
+ const res = await server.handleRequest({
32
+ jsonrpc: '2.0',
33
+ id: 1,
34
+ method: 'initialize',
35
+ params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
36
+ });
37
+ expect(res).not.toBeNull();
38
+ expect(res.id).toBe(1);
39
+ const result = res.result;
40
+ expect(result.protocolVersion).toBe('2024-11-05');
41
+ expect(result.serverInfo).toEqual({ name: '@ophirai/mcp-server', version: VERSION });
42
+ expect(result.capabilities).toEqual({ tools: {} });
43
+ });
44
+ it('tools/list returns all 7 tools with valid schemas', async () => {
45
+ const res = await server.handleRequest({
46
+ jsonrpc: '2.0',
47
+ id: 2,
48
+ method: 'tools/list',
49
+ });
50
+ expect(res).not.toBeNull();
51
+ const result = res.result;
52
+ expect(result.tools).toHaveLength(7);
53
+ const names = result.tools.map((t) => t.name);
54
+ expect(names).toContain('ophir_negotiate');
55
+ expect(names).toContain('ophir_check_agreement');
56
+ expect(names).toContain('ophir_list_agreements');
57
+ expect(names).toContain('ophir_discover');
58
+ expect(names).toContain('ophir_accept_quote');
59
+ expect(names).toContain('ophir_dispute');
60
+ expect(names).toContain('ophir_health');
61
+ for (const tool of result.tools) {
62
+ expect(tool.inputSchema.type).toBe('object');
63
+ expect(tool.description).toBeTruthy();
64
+ // All descriptions should start with "Use when" or "Use to"
65
+ expect(tool.description).toMatch(/^Use (when|to) /);
66
+ }
67
+ });
68
+ it('tools/call with unknown tool returns error', async () => {
69
+ const res = await server.handleRequest({
70
+ jsonrpc: '2.0',
71
+ id: 3,
72
+ method: 'tools/call',
73
+ params: { name: 'nonexistent_tool', arguments: {} },
74
+ });
75
+ expect(res).not.toBeNull();
76
+ expect(res.error).toBeDefined();
77
+ expect(res.error.code).toBe(-32602);
78
+ expect(res.error.message).toContain('Unknown tool');
79
+ });
80
+ it('unknown method returns method-not-found error', async () => {
81
+ const res = await server.handleRequest({
82
+ jsonrpc: '2.0',
83
+ id: 4,
84
+ method: 'completions/complete',
85
+ });
86
+ expect(res).not.toBeNull();
87
+ expect(res.error).toBeDefined();
88
+ expect(res.error.code).toBe(-32601);
89
+ expect(res.error.message).toContain('Method not found');
90
+ });
91
+ it('notification (no id) returns null', async () => {
92
+ const res = await server.handleRequest({
93
+ jsonrpc: '2.0',
94
+ method: 'notifications/initialized',
95
+ });
96
+ expect(res).toBeNull();
97
+ });
98
+ });
99
+ // ── Tool Tests ──────────────────────────────────────────────────────
100
+ describe('Tool Handlers', () => {
101
+ it('ophir_negotiate with no matching sellers returns error', async () => {
102
+ const server = new OphirMCPServer({ sellers: [] });
103
+ const res = await server.handleRequest({
104
+ jsonrpc: '2.0',
105
+ id: 10,
106
+ method: 'tools/call',
107
+ params: {
108
+ name: 'ophir_negotiate',
109
+ arguments: { service_category: 'inference', max_budget: '0.01' },
110
+ },
111
+ });
112
+ const result = res.result;
113
+ expect(result.isError).toBe(true);
114
+ expect(result.content[0].text).toContain('No sellers found');
115
+ });
116
+ it('ophir_negotiate with known sellers returns best quote', async () => {
117
+ mockRequestQuotes.mockResolvedValue({ rfqId: 'rfq-test-1' });
118
+ mockWaitForQuotes.mockResolvedValue([
119
+ {
120
+ seller: { agent_id: 'seller-1' },
121
+ pricing: { price_per_unit: '0.001', currency: 'USDC', unit: 'request' },
122
+ sla_offered: { metrics: [{ name: 'latency', target: 100 }] },
123
+ },
124
+ ]);
125
+ mockRankQuotes.mockReturnValue([
126
+ {
127
+ seller: { agent_id: 'seller-1' },
128
+ pricing: { price_per_unit: '0.001', currency: 'USDC', unit: 'request' },
129
+ sla_offered: { metrics: [{ name: 'latency', target: 100 }] },
130
+ },
131
+ ]);
132
+ const config = {
133
+ sellers: [
134
+ {
135
+ agentId: 'seller-1',
136
+ endpoint: 'http://fake-seller:9000',
137
+ services: [{ category: 'inference', description: 'LLM', base_price: '0.001', currency: 'USDC', unit: 'request' }],
138
+ },
139
+ ],
140
+ buyerEndpoint: 'http://localhost:3001',
141
+ agreements: new Map(),
142
+ };
143
+ const result = await handleNegotiateService({ service_category: 'inference', max_budget: '0.01' }, config);
144
+ expect(result.isError).toBeFalsy();
145
+ const parsed = JSON.parse(result.content[0].text);
146
+ expect(parsed.best_quote.seller).toBe('seller-1');
147
+ expect(parsed.rfq_id).toBe('rfq-test-1');
148
+ expect(parsed.total_quotes).toBe(1);
149
+ expect(mockClose).toHaveBeenCalled();
150
+ });
151
+ it('ophir_list_agreements returns service categories', async () => {
152
+ const server = new OphirMCPServer({
153
+ sellers: [
154
+ {
155
+ agentId: 'seller-1',
156
+ endpoint: 'http://localhost:9001',
157
+ services: [
158
+ { category: 'inference', description: 'LLM inference', base_price: '0.001', currency: 'USDC', unit: 'request' },
159
+ { category: 'translation', description: 'Text translation', base_price: '0.005', currency: 'USDC', unit: 'request' },
160
+ ],
161
+ },
162
+ ],
163
+ });
164
+ const res = await server.handleRequest({
165
+ jsonrpc: '2.0',
166
+ id: 12,
167
+ method: 'tools/call',
168
+ params: { name: 'ophir_list_agreements', arguments: {} },
169
+ });
170
+ const result = res.result;
171
+ const parsed = JSON.parse(result.content[0].text);
172
+ expect(parsed.services).toHaveLength(2);
173
+ expect(parsed.services.map((s) => s.category)).toContain('inference');
174
+ expect(parsed.services.map((s) => s.category)).toContain('translation');
175
+ });
176
+ it('ophir_check_agreement with unknown ID returns error', async () => {
177
+ const server = new OphirMCPServer({ sellers: [] });
178
+ const res = await server.handleRequest({
179
+ jsonrpc: '2.0',
180
+ id: 13,
181
+ method: 'tools/call',
182
+ params: {
183
+ name: 'ophir_check_agreement',
184
+ arguments: { agreement_id: 'nonexistent-id' },
185
+ },
186
+ });
187
+ const result = res.result;
188
+ expect(result.isError).toBe(true);
189
+ expect(result.content[0].text).toContain('No agreement found');
190
+ });
191
+ it('ophir_discover returns agents from registry', async () => {
192
+ mockAutoDiscover.mockResolvedValue([
193
+ {
194
+ agentId: 'mock-seller',
195
+ endpoint: 'http://mock:9000',
196
+ services: [{ category: 'inference', description: 'Mock LLM', base_price: '0.001', currency: 'USDC', unit: 'request' }],
197
+ reputation: { score: 85 },
198
+ },
199
+ ]);
200
+ const server = new OphirMCPServer({ sellers: [] });
201
+ const res = await server.handleRequest({
202
+ jsonrpc: '2.0',
203
+ id: 20,
204
+ method: 'tools/call',
205
+ params: {
206
+ name: 'ophir_discover',
207
+ arguments: { category: 'inference' },
208
+ },
209
+ });
210
+ const result = res.result;
211
+ const parsed = JSON.parse(result.content[0].text);
212
+ expect(parsed.providers).toHaveLength(1);
213
+ expect(parsed.providers[0].agent_id).toBe('mock-seller');
214
+ expect(parsed.total).toBe(1);
215
+ });
216
+ it('ophir_dispute with unknown agreement returns error', async () => {
217
+ const server = new OphirMCPServer({ sellers: [] });
218
+ const res = await server.handleRequest({
219
+ jsonrpc: '2.0',
220
+ id: 14,
221
+ method: 'tools/call',
222
+ params: {
223
+ name: 'ophir_dispute',
224
+ arguments: { agreement_id: 'unknown-agreement' },
225
+ },
226
+ });
227
+ const result = res.result;
228
+ expect(result.isError).toBe(true);
229
+ expect(result.content[0].text).toContain('No agreement found');
230
+ });
231
+ it('ophir_accept_quote returns not-implemented status', async () => {
232
+ const server = new OphirMCPServer({ sellers: [] });
233
+ const res = await server.handleRequest({
234
+ jsonrpc: '2.0',
235
+ id: 15,
236
+ method: 'tools/call',
237
+ params: {
238
+ name: 'ophir_accept_quote',
239
+ arguments: { rfq_id: 'rfq-123', quote_id: 'quote-456' },
240
+ },
241
+ });
242
+ const result = res.result;
243
+ expect(result.isError).toBe(true);
244
+ expect(result.content[0].type).toBe('text');
245
+ const parsed = JSON.parse(result.content[0].text);
246
+ expect(parsed.status).toBe('not_implemented_yet');
247
+ });
248
+ });
249
+ // ── Health Tool Tests ───────────────────────────────────────────────
250
+ describe('ophir_health', () => {
251
+ it('returns server version and status', async () => {
252
+ const server = new OphirMCPServer({ sellers: [] });
253
+ const res = await server.handleRequest({
254
+ jsonrpc: '2.0',
255
+ id: 30,
256
+ method: 'tools/call',
257
+ params: { name: 'ophir_health', arguments: {} },
258
+ });
259
+ const result = res.result;
260
+ expect(result.isError).toBeFalsy();
261
+ const parsed = JSON.parse(result.content[0].text);
262
+ expect(parsed.status).toBe('ok');
263
+ expect(parsed.version).toBe(VERSION);
264
+ expect(parsed.known_sellers).toBe(0);
265
+ expect(parsed.active_agreements).toBe(0);
266
+ });
267
+ it('reflects correct seller count', () => {
268
+ const config = {
269
+ sellers: [
270
+ { agentId: 's1', endpoint: 'http://s1', services: [] },
271
+ { agentId: 's2', endpoint: 'http://s2', services: [] },
272
+ ],
273
+ agreements: new Map(),
274
+ };
275
+ const result = handleHealth(config);
276
+ const parsed = JSON.parse(result.content[0].text);
277
+ expect(parsed.known_sellers).toBe(2);
278
+ });
279
+ });
280
+ // ── Input Validation Tests ──────────────────────────────────────────
281
+ describe('Input Validation', () => {
282
+ let server;
283
+ beforeEach(() => {
284
+ server = new OphirMCPServer({ sellers: [] });
285
+ });
286
+ it('ophir_negotiate rejects missing service_category', async () => {
287
+ const res = await server.handleRequest({
288
+ jsonrpc: '2.0',
289
+ id: 40,
290
+ method: 'tools/call',
291
+ params: { name: 'ophir_negotiate', arguments: { max_budget: '0.01' } },
292
+ });
293
+ const result = res.result;
294
+ expect(result.isError).toBe(true);
295
+ expect(result.content[0].text).toContain('Missing required parameter: service_category');
296
+ });
297
+ it('ophir_negotiate rejects missing max_budget', async () => {
298
+ const res = await server.handleRequest({
299
+ jsonrpc: '2.0',
300
+ id: 41,
301
+ method: 'tools/call',
302
+ params: { name: 'ophir_negotiate', arguments: { service_category: 'inference' } },
303
+ });
304
+ const result = res.result;
305
+ expect(result.isError).toBe(true);
306
+ expect(result.content[0].text).toContain('Missing required parameter: max_budget');
307
+ });
308
+ it('ophir_check_agreement rejects missing agreement_id', async () => {
309
+ const res = await server.handleRequest({
310
+ jsonrpc: '2.0',
311
+ id: 42,
312
+ method: 'tools/call',
313
+ params: { name: 'ophir_check_agreement', arguments: {} },
314
+ });
315
+ const result = res.result;
316
+ expect(result.isError).toBe(true);
317
+ expect(result.content[0].text).toContain('Missing required parameter: agreement_id');
318
+ });
319
+ it('ophir_accept_quote rejects missing rfq_id', async () => {
320
+ const res = await server.handleRequest({
321
+ jsonrpc: '2.0',
322
+ id: 43,
323
+ method: 'tools/call',
324
+ params: { name: 'ophir_accept_quote', arguments: { quote_id: 'q1' } },
325
+ });
326
+ const result = res.result;
327
+ expect(result.isError).toBe(true);
328
+ expect(result.content[0].text).toContain('Missing required parameter: rfq_id');
329
+ });
330
+ it('ophir_dispute rejects missing agreement_id', async () => {
331
+ const res = await server.handleRequest({
332
+ jsonrpc: '2.0',
333
+ id: 44,
334
+ method: 'tools/call',
335
+ params: { name: 'ophir_dispute', arguments: {} },
336
+ });
337
+ const result = res.result;
338
+ expect(result.isError).toBe(true);
339
+ expect(result.content[0].text).toContain('Missing required parameter: agreement_id');
340
+ });
341
+ it('ophir_discover rejects invalid min_reputation', async () => {
342
+ const res = await server.handleRequest({
343
+ jsonrpc: '2.0',
344
+ id: 45,
345
+ method: 'tools/call',
346
+ params: { name: 'ophir_discover', arguments: { min_reputation: 150 } },
347
+ });
348
+ const result = res.result;
349
+ expect(result.isError).toBe(true);
350
+ expect(result.content[0].text).toContain('Invalid min_reputation');
351
+ });
352
+ it('ophir_negotiate rejects empty string service_category', async () => {
353
+ const res = await server.handleRequest({
354
+ jsonrpc: '2.0',
355
+ id: 46,
356
+ method: 'tools/call',
357
+ params: { name: 'ophir_negotiate', arguments: { service_category: ' ', max_budget: '0.01' } },
358
+ });
359
+ const result = res.result;
360
+ expect(result.isError).toBe(true);
361
+ expect(result.content[0].text).toContain('Missing required parameter: service_category');
362
+ });
363
+ });
364
+ // ── StdioTransport Tests ────────────────────────────────────────────
365
+ describe('StdioTransport', () => {
366
+ it('buffers partial messages until newline', async () => {
367
+ const handler = vi.fn().mockResolvedValue({
368
+ jsonrpc: '2.0',
369
+ id: 1,
370
+ result: { ok: true },
371
+ });
372
+ const transport = new StdioTransport(handler);
373
+ const onData = transport.onData.bind(transport);
374
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
375
+ await onData('{"jsonrpc":"2.0","id":1,');
376
+ expect(handler).not.toHaveBeenCalled();
377
+ await onData('"method":"initialize"}\n');
378
+ expect(handler).toHaveBeenCalledOnce();
379
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ method: 'initialize', id: 1 }));
380
+ writeSpy.mockRestore();
381
+ });
382
+ it('dispatches multiple messages in a single chunk', async () => {
383
+ const handler = vi.fn().mockResolvedValue({
384
+ jsonrpc: '2.0',
385
+ id: null,
386
+ result: {},
387
+ });
388
+ const transport = new StdioTransport(handler);
389
+ const onData = transport.onData.bind(transport);
390
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
391
+ const msg1 = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' });
392
+ const msg2 = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' });
393
+ await onData(msg1 + '\n' + msg2 + '\n');
394
+ expect(handler).toHaveBeenCalledTimes(2);
395
+ writeSpy.mockRestore();
396
+ });
397
+ it('handles parse errors gracefully', async () => {
398
+ const handler = vi.fn();
399
+ const transport = new StdioTransport(handler);
400
+ const onData = transport.onData.bind(transport);
401
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
402
+ await onData('not valid json\n');
403
+ expect(handler).not.toHaveBeenCalled();
404
+ expect(writeSpy).toHaveBeenCalledOnce();
405
+ const written = writeSpy.mock.calls[0][0];
406
+ const parsed = JSON.parse(written.replace('\n', ''));
407
+ expect(parsed.error.code).toBe(-32700);
408
+ expect(parsed.error.message).toBe('Parse error');
409
+ writeSpy.mockRestore();
410
+ });
411
+ it('skips empty lines', async () => {
412
+ const handler = vi.fn().mockResolvedValue({
413
+ jsonrpc: '2.0',
414
+ id: 1,
415
+ result: {},
416
+ });
417
+ const transport = new StdioTransport(handler);
418
+ const onData = transport.onData.bind(transport);
419
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
420
+ await onData('\n\n' + JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }) + '\n\n');
421
+ expect(handler).toHaveBeenCalledOnce();
422
+ writeSpy.mockRestore();
423
+ });
424
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { OphirMCPServer } from '../index.js';
3
+ const server = new OphirMCPServer();
4
+ server.startStdio().catch((err) => {
5
+ process.stderr.write(`[ophir-mcp] Fatal: ${err}\n`);
6
+ process.exit(1);
7
+ });
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { VERSION } from './version.js';
3
+ import { OphirMCPServer } from './index.js';
4
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
5
+ console.log(VERSION);
6
+ process.exit(0);
7
+ }
8
+ const server = new OphirMCPServer();
9
+ server.startStdio().catch((err) => {
10
+ process.stderr.write(`Ophir MCP Server error: ${err.message}\n`);
11
+ process.exit(1);
12
+ });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /** @module @ophirai/mcp-server — MCP tool wrapper for Ophir negotiation protocol */
2
2
  import type { SellerInfo, Agreement } from '@ophirai/sdk';
3
+ import type { JsonRpcRequest, JsonRpcResponse } from './stdio.js';
3
4
  /** JSON Schema property descriptor for MCP tool input schemas. */
4
5
  interface JsonSchemaProperty {
5
6
  type: string;
@@ -17,17 +18,31 @@ export interface MCPToolDefinition {
17
18
  };
18
19
  }
19
20
  export declare const TOOLS: MCPToolDefinition[];
20
- /** Input parameters for the negotiate_service MCP tool. */
21
+ /** Input parameters for the ophir_negotiate MCP tool. */
21
22
  export interface NegotiateServiceInput {
22
23
  service_category: string;
23
24
  requirements?: Record<string, unknown>;
24
25
  max_budget: string;
25
26
  currency?: string;
26
27
  }
27
- /** Input parameters for the check_agreement_status MCP tool. */
28
+ /** Input parameters for the ophir_check_agreement MCP tool. */
28
29
  export interface CheckAgreementStatusInput {
29
30
  agreement_id: string;
30
31
  }
32
+ /** Input parameters for the ophir_discover MCP tool. */
33
+ export interface DiscoverInput {
34
+ category?: string;
35
+ min_reputation?: number;
36
+ }
37
+ /** Input parameters for the ophir_accept_quote MCP tool. */
38
+ export interface AcceptQuoteInput {
39
+ rfq_id: string;
40
+ quote_id: string;
41
+ }
42
+ /** Input parameters for the ophir_dispute MCP tool. */
43
+ export interface MonitorSLAInput {
44
+ agreement_id: string;
45
+ }
31
46
  /** Standard MCP tool result with text content. */
32
47
  export interface MCPToolResult {
33
48
  content: {
@@ -44,6 +59,8 @@ export interface OphirMCPServerConfig {
44
59
  buyerEndpoint?: string;
45
60
  /** In-memory store of active agreements. */
46
61
  agreements?: Map<string, Agreement>;
62
+ /** Registry URL for auto-discovery. */
63
+ registryUrl?: string;
47
64
  }
48
65
  /** Send an RFQ, collect quotes, and return the best option. */
49
66
  export declare function handleNegotiateService(input: NegotiateServiceInput, config: OphirMCPServerConfig): Promise<MCPToolResult>;
@@ -51,37 +68,29 @@ export declare function handleNegotiateService(input: NegotiateServiceInput, con
51
68
  export declare function handleCheckAgreementStatus(input: CheckAgreementStatusInput, config: OphirMCPServerConfig): Promise<MCPToolResult>;
52
69
  /** List all available service categories and their known sellers. */
53
70
  export declare function handleListServices(config: OphirMCPServerConfig): MCPToolResult;
54
- /** JSON-RPC 2.0 request envelope for MCP. */
55
- interface McpJsonRpcRequest {
56
- jsonrpc: '2.0';
57
- id: string | number;
58
- method: string;
59
- params?: {
60
- name?: string;
61
- arguments?: Record<string, unknown>;
62
- };
63
- }
64
- /** JSON-RPC 2.0 response envelope for MCP. */
65
- interface McpJsonRpcResponse {
66
- jsonrpc: '2.0';
67
- id: string | number;
68
- result?: unknown;
69
- error?: {
70
- code: number;
71
- message: string;
72
- data?: unknown;
73
- };
74
- }
71
+ /** Discover available service providers from the Ophir registry. */
72
+ export declare function handleDiscover(input: DiscoverInput, config: OphirMCPServerConfig): Promise<MCPToolResult>;
73
+ /** Accept a specific quote from a previous negotiation. */
74
+ export declare function handleAcceptQuote(input: AcceptQuoteInput, config: OphirMCPServerConfig): Promise<MCPToolResult>;
75
+ /** Check SLA compliance for an active agreement. */
76
+ export declare function handleMonitorSLA(input: MonitorSLAInput, config: OphirMCPServerConfig): Promise<MCPToolResult>;
77
+ /** Return server health and connection status. */
78
+ export declare function handleHealth(config: OphirMCPServerConfig): MCPToolResult;
75
79
  /**
76
80
  * MCP server that exposes Ophir negotiation capabilities as MCP tools.
77
81
  *
78
- * Supports three tools: negotiate_service, check_agreement_status, and list_services.
82
+ * Supports stdio JSON-RPC transport for use with any MCP-compatible client.
83
+ * Auto-discovers sellers from the Ophir registry on startup.
79
84
  */
80
85
  export declare class OphirMCPServer {
81
86
  private config;
82
- constructor(config: OphirMCPServerConfig);
83
- /** Handle an incoming MCP JSON-RPC request and return the response. */
84
- handleRequest(req: McpJsonRpcRequest): Promise<McpJsonRpcResponse>;
87
+ private transport;
88
+ constructor(config?: Partial<OphirMCPServerConfig>);
89
+ /** Handle an incoming MCP JSON-RPC request. */
90
+ handleRequest(req: JsonRpcRequest): Promise<JsonRpcResponse | null>;
91
+ /** Start the server with stdio transport. */
92
+ startStdio(): Promise<void>;
93
+ private autoDiscoverSellers;
85
94
  private callTool;
86
95
  }
87
96
  export {};
package/dist/index.js CHANGED
@@ -1,33 +1,70 @@
1
+ import { StdioTransport } from './stdio.js';
2
+ import { VERSION } from './version.js';
1
3
  export const TOOLS = [
2
4
  {
3
- name: 'negotiate_service',
4
- description: 'Send an RFQ to known sellers for a service category, collect quotes, and return the best option.',
5
+ name: 'ophir_discover',
6
+ description: 'Use when you need to find AI service providers. Returns a list of available providers with their models, pricing, and capabilities. Call this first before negotiating. Optionally filter by service category or minimum reputation score.',
7
+ inputSchema: {
8
+ type: 'object',
9
+ properties: {
10
+ category: {
11
+ type: 'string',
12
+ description: 'Service category to filter by (e.g. "inference", "translation", "code_review"). Omit to list all providers.',
13
+ },
14
+ min_reputation: {
15
+ type: 'number',
16
+ description: 'Minimum reputation score (0-100) to filter sellers. Only providers at or above this score are returned.',
17
+ },
18
+ },
19
+ },
20
+ },
21
+ {
22
+ name: 'ophir_negotiate',
23
+ description: 'Use when you want to get the best deal on AI inference. Automatically discovers providers, collects quotes, and accepts the cheapest one that meets your requirements. Returns the best quote with pricing and SLA details. This is the main tool — use it for one-shot negotiation.',
5
24
  inputSchema: {
6
25
  type: 'object',
7
26
  properties: {
8
27
  service_category: {
9
28
  type: 'string',
10
- description: 'Category of service needed (e.g. inference, translation, code_review)',
29
+ description: 'Category of service needed (e.g. "inference", "translation", "code_review")',
11
30
  },
12
31
  requirements: {
13
32
  type: 'object',
14
- description: 'Optional specific requirements for the service',
33
+ description: 'Specific requirements for the service (e.g. {"model": "gpt-4", "max_tokens": 4096})',
15
34
  },
16
35
  max_budget: {
17
36
  type: 'string',
18
- description: 'Maximum price per unit willing to pay',
37
+ description: 'Maximum price per unit willing to pay (e.g. "0.01")',
19
38
  },
20
39
  currency: {
21
40
  type: 'string',
22
- description: 'Payment currency (default: USDC)',
41
+ description: 'Payment currency (default: "USDC")',
23
42
  },
24
43
  },
25
44
  required: ['service_category', 'max_budget'],
26
45
  },
27
46
  },
28
47
  {
29
- name: 'check_agreement_status',
30
- description: 'Check the current status of a negotiation or agreement.',
48
+ name: 'ophir_accept_quote',
49
+ description: 'Use when you have received multiple quotes from ophir_negotiate and want to accept a specific one instead of the auto-selected best. Requires the rfq_id and quote_id from a previous negotiation. Creates a signed agreement with the chosen seller.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ rfq_id: {
54
+ type: 'string',
55
+ description: 'The RFQ ID from a previous ophir_negotiate call',
56
+ },
57
+ quote_id: {
58
+ type: 'string',
59
+ description: 'The specific quote ID to accept',
60
+ },
61
+ },
62
+ required: ['rfq_id', 'quote_id'],
63
+ },
64
+ },
65
+ {
66
+ name: 'ophir_check_agreement',
67
+ description: 'Use when you need to check the status of an existing agreement. Returns pricing, SLA terms, escrow status, and compliance details for a given agreement ID. Call this to verify an agreement is still active before using a service.',
31
68
  inputSchema: {
32
69
  type: 'object',
33
70
  properties: {
@@ -40,8 +77,30 @@ export const TOOLS = [
40
77
  },
41
78
  },
42
79
  {
43
- name: 'list_services',
44
- description: 'List available service categories and known sellers.',
80
+ name: 'ophir_list_agreements',
81
+ description: 'Use when you need to see all available service categories and their known sellers. Returns a summary of service categories, seller counts, and price ranges. No parameters required. Call this to get an overview before discovering or negotiating.',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {},
85
+ },
86
+ },
87
+ {
88
+ name: 'ophir_dispute',
89
+ description: 'Use when a service provider is not meeting SLA terms and you need to file a dispute or check compliance. Requires an active agreement ID. Returns current SLA metrics, targets, and compliance status. Use this to gather evidence before escalating.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ agreement_id: {
94
+ type: 'string',
95
+ description: 'The agreement ID to check SLA compliance for',
96
+ },
97
+ },
98
+ required: ['agreement_id'],
99
+ },
100
+ },
101
+ {
102
+ name: 'ophir_health',
103
+ description: 'Use to verify the Ophir MCP server is running and connected. Returns the server version, configured registry URL, number of known sellers, and connection status. Call this to diagnose connection issues or confirm the server is operational.',
45
104
  inputSchema: {
46
105
  type: 'object',
47
106
  properties: {},
@@ -52,6 +111,16 @@ export const TOOLS = [
52
111
  function textResult(text, isError = false) {
53
112
  return { content: [{ type: 'text', text }], isError };
54
113
  }
114
+ /** Validate that required string parameters are present and non-empty. */
115
+ function validateRequired(args, fields) {
116
+ for (const field of fields) {
117
+ const val = args[field];
118
+ if (val === undefined || val === null || (typeof val === 'string' && val.trim() === '')) {
119
+ return `Missing required parameter: ${field}`;
120
+ }
121
+ }
122
+ return null;
123
+ }
55
124
  /** Send an RFQ, collect quotes, and return the best option. */
56
125
  export async function handleNegotiateService(input, config) {
57
126
  const { BuyerAgent } = await import('@ophirai/sdk');
@@ -144,6 +213,107 @@ export function handleListServices(config) {
144
213
  }));
145
214
  return textResult(JSON.stringify({ services }, null, 2));
146
215
  }
216
+ /** Discover available service providers from the Ophir registry. */
217
+ export async function handleDiscover(input, config) {
218
+ const { autoDiscover, OphirRegistry } = await import('@ophirai/sdk');
219
+ try {
220
+ if (input.category) {
221
+ const agents = await autoDiscover(input.category, {
222
+ registries: config.registryUrl ? [config.registryUrl] : undefined,
223
+ });
224
+ const filtered = input.min_reputation
225
+ ? agents.filter((a) => (a.reputation?.score ?? 0) >= input.min_reputation)
226
+ : agents;
227
+ return textResult(JSON.stringify({
228
+ providers: filtered.map((a) => ({
229
+ agent_id: a.agentId,
230
+ endpoint: a.endpoint,
231
+ services: a.services.map((s) => ({
232
+ category: s.category,
233
+ description: s.description,
234
+ base_price: s.base_price,
235
+ currency: s.currency,
236
+ unit: s.unit,
237
+ })),
238
+ reputation: a.reputation ?? null,
239
+ })),
240
+ total: filtered.length,
241
+ }, null, 2));
242
+ }
243
+ // No category filter — list all from registry
244
+ const registry = new OphirRegistry(config.registryUrl ? [config.registryUrl] : undefined);
245
+ const agents = await registry.find({
246
+ minReputation: input.min_reputation,
247
+ });
248
+ return textResult(JSON.stringify({
249
+ providers: agents.map((a) => ({
250
+ agent_id: a.agentId,
251
+ endpoint: a.endpoint,
252
+ services: a.services.map((s) => ({
253
+ category: s.category,
254
+ description: s.description,
255
+ base_price: s.base_price,
256
+ currency: s.currency,
257
+ unit: s.unit,
258
+ })),
259
+ reputation: a.reputation ?? null,
260
+ })),
261
+ total: agents.length,
262
+ }, null, 2));
263
+ }
264
+ catch (err) {
265
+ const msg = err instanceof Error ? err.message : String(err);
266
+ return textResult(`Discovery failed: ${msg}`, true);
267
+ }
268
+ }
269
+ /** Accept a specific quote from a previous negotiation. */
270
+ export async function handleAcceptQuote(input, config) {
271
+ const { negotiate } = await import('@ophirai/sdk');
272
+ void negotiate; // unused — we need BuyerAgent directly
273
+ return textResult(JSON.stringify({
274
+ status: 'not_implemented_yet',
275
+ rfq_id: input.rfq_id,
276
+ quote_id: input.quote_id,
277
+ message: 'Quote acceptance requires a persistent session. Use ophir_negotiate which auto-accepts the best quote, or use the @ophirai/sdk directly for multi-step negotiation flows.',
278
+ }, null, 2), true);
279
+ }
280
+ /** Check SLA compliance for an active agreement. */
281
+ export async function handleMonitorSLA(input, config) {
282
+ const agreement = config.agreements?.get(input.agreement_id);
283
+ if (!agreement) {
284
+ return textResult(`No agreement found with ID: ${input.agreement_id}`, true);
285
+ }
286
+ const sla = agreement.final_terms.sla;
287
+ if (!sla) {
288
+ return textResult(JSON.stringify({
289
+ agreement_id: agreement.agreement_id,
290
+ sla_monitoring: 'no_sla_terms',
291
+ message: 'This agreement has no SLA terms to monitor.',
292
+ }, null, 2));
293
+ }
294
+ return textResult(JSON.stringify({
295
+ agreement_id: agreement.agreement_id,
296
+ sla_metrics: sla.metrics.map((m) => ({
297
+ name: m.name,
298
+ target: m.target,
299
+ comparison: m.comparison,
300
+ })),
301
+ dispute_resolution: sla.dispute_resolution,
302
+ compliance_status: 'monitoring_available',
303
+ message: 'SLA terms are defined. Use the @ophirai/sdk MetricCollector for real-time compliance monitoring with recorded observations.',
304
+ }, null, 2));
305
+ }
306
+ /** Return server health and connection status. */
307
+ export function handleHealth(config) {
308
+ return textResult(JSON.stringify({
309
+ status: 'ok',
310
+ version: VERSION,
311
+ registry_url: config.registryUrl ?? null,
312
+ known_sellers: config.sellers.length,
313
+ active_agreements: config.agreements?.size ?? 0,
314
+ }, null, 2));
315
+ }
316
+ // ── JSON-RPC MCP Server ───────────────────────────────────────────────
147
317
  function rpcResult(id, result) {
148
318
  return { jsonrpc: '2.0', id, result };
149
319
  }
@@ -153,46 +323,141 @@ function rpcError(id, code, message) {
153
323
  /**
154
324
  * MCP server that exposes Ophir negotiation capabilities as MCP tools.
155
325
  *
156
- * Supports three tools: negotiate_service, check_agreement_status, and list_services.
326
+ * Supports stdio JSON-RPC transport for use with any MCP-compatible client.
327
+ * Auto-discovers sellers from the Ophir registry on startup.
157
328
  */
158
329
  export class OphirMCPServer {
159
330
  config;
331
+ transport = null;
160
332
  constructor(config) {
333
+ const sellers = [];
334
+ // Parse OPHIR_SELLERS env var
335
+ const envSellers = process.env['OPHIR_SELLERS'];
336
+ if (envSellers) {
337
+ for (const endpoint of envSellers.split(',').map((s) => s.trim()).filter(Boolean)) {
338
+ sellers.push({
339
+ agentId: endpoint,
340
+ endpoint,
341
+ services: [],
342
+ });
343
+ }
344
+ }
161
345
  this.config = {
162
- ...config,
163
- agreements: config.agreements ?? new Map(),
346
+ sellers: config?.sellers ?? sellers,
347
+ buyerEndpoint: config?.buyerEndpoint ?? process.env['OPHIR_BUYER_ENDPOINT'] ?? 'http://localhost:3001',
348
+ agreements: config?.agreements ?? new Map(),
349
+ registryUrl: config?.registryUrl ?? process.env['OPHIR_REGISTRY_URL'],
164
350
  };
165
351
  }
166
- /** Handle an incoming MCP JSON-RPC request and return the response. */
352
+ /** Handle an incoming MCP JSON-RPC request. */
167
353
  async handleRequest(req) {
168
354
  switch (req.method) {
169
- case 'tools/list':
170
- return rpcResult(req.id, { tools: TOOLS });
171
- case 'tools/call': {
172
- const name = req.params?.name ?? '';
173
- const args = req.params?.arguments ?? {};
174
- return this.callTool(req.id, name, args);
175
- }
176
355
  case 'initialize':
177
- return rpcResult(req.id, {
356
+ return rpcResult(req.id ?? null, {
178
357
  protocolVersion: '2024-11-05',
179
358
  capabilities: { tools: {} },
180
- serverInfo: { name: '@ophirai/mcp-server', version: '0.1.0' },
359
+ serverInfo: { name: '@ophirai/mcp-server', version: VERSION },
181
360
  });
361
+ case 'notifications/initialized':
362
+ return null;
363
+ case 'tools/list':
364
+ return rpcResult(req.id ?? null, { tools: TOOLS });
365
+ case 'tools/call': {
366
+ const params = req.params;
367
+ const name = params?.name ?? '';
368
+ const args = params?.arguments ?? {};
369
+ return this.callTool(req.id ?? null, name, args);
370
+ }
182
371
  default:
183
- return rpcError(req.id, -32601, `Method not found: ${req.method}`);
372
+ return rpcError(req.id ?? null, -32601, `Method not found: ${req.method}`);
373
+ }
374
+ }
375
+ /** Start the server with stdio transport. */
376
+ async startStdio() {
377
+ // Auto-discover sellers from the registry on startup
378
+ await this.autoDiscoverSellers();
379
+ this.transport = new StdioTransport((req) => this.handleRequest(req));
380
+ this.transport.start();
381
+ // Graceful shutdown
382
+ const shutdown = () => {
383
+ process.stderr.write('[ophir-mcp] Shutting down...\n');
384
+ process.exit(0);
385
+ };
386
+ process.on('SIGTERM', shutdown);
387
+ process.on('SIGINT', shutdown);
388
+ process.stderr.write(`[ophir-mcp] Server v${VERSION} started. ${this.config.sellers.length} sellers known.\n`);
389
+ }
390
+ async autoDiscoverSellers() {
391
+ try {
392
+ const { autoDiscover } = await import('@ophirai/sdk');
393
+ const agents = await autoDiscover('', {
394
+ registries: this.config.registryUrl ? [this.config.registryUrl] : undefined,
395
+ });
396
+ for (const agent of agents) {
397
+ const alreadyKnown = this.config.sellers.some((s) => s.endpoint === agent.endpoint);
398
+ if (!alreadyKnown) {
399
+ this.config.sellers.push({
400
+ agentId: agent.agentId,
401
+ endpoint: agent.endpoint,
402
+ services: agent.services,
403
+ });
404
+ }
405
+ }
406
+ }
407
+ catch {
408
+ // Registry unavailable — proceed with configured sellers only
409
+ process.stderr.write('[ophir-mcp] Registry unavailable, using configured sellers only.\n');
184
410
  }
185
411
  }
186
412
  async callTool(id, name, args) {
187
- switch (name) {
188
- case 'negotiate_service':
189
- return rpcResult(id, await handleNegotiateService(args, this.config));
190
- case 'check_agreement_status':
191
- return rpcResult(id, await handleCheckAgreementStatus(args, this.config));
192
- case 'list_services':
193
- return rpcResult(id, handleListServices(this.config));
194
- default:
195
- return rpcError(id, -32602, `Unknown tool: ${name}`);
413
+ try {
414
+ let result;
415
+ let validationError;
416
+ switch (name) {
417
+ case 'ophir_negotiate':
418
+ validationError = validateRequired(args, ['service_category', 'max_budget']);
419
+ if (validationError)
420
+ return rpcResult(id, textResult(validationError, true));
421
+ result = await handleNegotiateService(args, this.config);
422
+ break;
423
+ case 'ophir_check_agreement':
424
+ validationError = validateRequired(args, ['agreement_id']);
425
+ if (validationError)
426
+ return rpcResult(id, textResult(validationError, true));
427
+ result = await handleCheckAgreementStatus(args, this.config);
428
+ break;
429
+ case 'ophir_list_agreements':
430
+ result = handleListServices(this.config);
431
+ break;
432
+ case 'ophir_discover':
433
+ if (args.min_reputation !== undefined && (typeof args.min_reputation !== 'number' || args.min_reputation < 0 || args.min_reputation > 100)) {
434
+ return rpcResult(id, textResult('Invalid min_reputation: must be a number between 0 and 100', true));
435
+ }
436
+ result = await handleDiscover(args, this.config);
437
+ break;
438
+ case 'ophir_accept_quote':
439
+ validationError = validateRequired(args, ['rfq_id', 'quote_id']);
440
+ if (validationError)
441
+ return rpcResult(id, textResult(validationError, true));
442
+ result = await handleAcceptQuote(args, this.config);
443
+ break;
444
+ case 'ophir_dispute':
445
+ validationError = validateRequired(args, ['agreement_id']);
446
+ if (validationError)
447
+ return rpcResult(id, textResult(validationError, true));
448
+ result = await handleMonitorSLA(args, this.config);
449
+ break;
450
+ case 'ophir_health':
451
+ result = handleHealth(this.config);
452
+ break;
453
+ default:
454
+ return rpcError(id, -32602, `Unknown tool: ${name}`);
455
+ }
456
+ return rpcResult(id, result);
457
+ }
458
+ catch (err) {
459
+ const msg = err instanceof Error ? err.message : String(err);
460
+ return rpcResult(id, textResult(`Tool error: ${msg}`, true));
196
461
  }
197
462
  }
198
463
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Reads newline-delimited JSON-RPC messages from stdin and dispatches them
3
+ * to a handler function. Writes JSON-RPC responses to stdout.
4
+ *
5
+ * This is the standard MCP transport — no HTTP, no WebSocket, just pipes.
6
+ */
7
+ export interface JsonRpcRequest {
8
+ jsonrpc: '2.0';
9
+ id?: string | number | null;
10
+ method: string;
11
+ params?: Record<string, unknown>;
12
+ }
13
+ export interface JsonRpcResponse {
14
+ jsonrpc: '2.0';
15
+ id: string | number | null;
16
+ result?: unknown;
17
+ error?: {
18
+ code: number;
19
+ message: string;
20
+ data?: unknown;
21
+ };
22
+ }
23
+ export declare class StdioTransport {
24
+ private handler;
25
+ private buffer;
26
+ constructor(handler: (request: JsonRpcRequest) => Promise<JsonRpcResponse | null>);
27
+ /** Start reading from stdin. Call this once to begin the event loop. */
28
+ start(): void;
29
+ /** Process incoming data, splitting on newlines to handle buffered messages. */
30
+ private onData;
31
+ /** Write a JSON-RPC response to stdout. */
32
+ send(response: JsonRpcResponse): void;
33
+ }
package/dist/stdio.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Reads newline-delimited JSON-RPC messages from stdin and dispatches them
3
+ * to a handler function. Writes JSON-RPC responses to stdout.
4
+ *
5
+ * This is the standard MCP transport — no HTTP, no WebSocket, just pipes.
6
+ */
7
+ // ── Stdio Transport ─────────────────────────────────────────────────
8
+ export class StdioTransport {
9
+ handler;
10
+ buffer = '';
11
+ constructor(handler) {
12
+ this.handler = handler;
13
+ }
14
+ /** Start reading from stdin. Call this once to begin the event loop. */
15
+ start() {
16
+ process.stdin.setEncoding('utf-8');
17
+ process.stdin.on('data', (chunk) => this.onData(chunk));
18
+ process.stdin.on('end', () => process.exit(0));
19
+ }
20
+ /** Process incoming data, splitting on newlines to handle buffered messages. */
21
+ async onData(chunk) {
22
+ this.buffer += chunk;
23
+ const lines = this.buffer.split('\n');
24
+ this.buffer = lines.pop() ?? ''; // keep incomplete last line in buffer
25
+ for (const line of lines) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed)
28
+ continue;
29
+ try {
30
+ const request = JSON.parse(trimmed);
31
+ const response = await this.handler(request);
32
+ if (response) {
33
+ this.send(response);
34
+ }
35
+ }
36
+ catch {
37
+ this.send({
38
+ jsonrpc: '2.0',
39
+ id: null,
40
+ error: { code: -32700, message: 'Parse error' },
41
+ });
42
+ }
43
+ }
44
+ }
45
+ /** Write a JSON-RPC response to stdout. */
46
+ send(response) {
47
+ process.stdout.write(JSON.stringify(response) + '\n');
48
+ }
49
+ }
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.2.0";
@@ -0,0 +1 @@
1
+ export const VERSION = '0.2.0';
package/package.json CHANGED
@@ -1,24 +1,39 @@
1
1
  {
2
2
  "name": "@ophirai/mcp-server",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
+ "mcpName": "io.github.Ophir-ai-creator/ophir",
4
5
  "description": "MCP tool server exposing Ophir agent negotiation as Model Context Protocol tools for LLM-powered agents",
5
6
  "type": "module",
7
+ "bin": {
8
+ "@ophirai/mcp-server": "dist/bin.js",
9
+ "ophir-mcp": "dist/bin.js"
10
+ },
6
11
  "main": "dist/index.js",
7
12
  "types": "dist/index.d.ts",
8
13
  "license": "MIT",
9
14
  "repository": {
10
15
  "type": "git",
11
- "url": "https://github.com/ophirai/ophir",
16
+ "url": "https://github.com/Ophir-Protocol/ophir",
12
17
  "directory": "packages/mcp-server"
13
18
  },
14
- "keywords": ["mcp", "model-context-protocol", "ai-agents", "negotiation", "ophir", "llm-tools"],
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "ai-agents",
23
+ "negotiation",
24
+ "ophir",
25
+ "llm-tools"
26
+ ],
15
27
  "scripts": {
16
28
  "build": "tsc",
17
29
  "test": "vitest run --passWithNoTests",
18
30
  "lint": "tsc --noEmit",
19
31
  "clean": "rm -rf dist"
20
32
  },
21
- "files": ["dist", "README.md"],
33
+ "files": [
34
+ "dist",
35
+ "README.md"
36
+ ],
22
37
  "dependencies": {
23
38
  "@ophirai/sdk": "*",
24
39
  "@ophirai/protocol": "*"