@prereason/mcp 0.3.2 → 0.5.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/CHANGELOG.md +36 -0
- package/README.md +64 -48
- package/bin/cli.js +97 -49
- package/lib/claim.js +233 -0
- package/lib/credentials.js +152 -0
- package/lib/jsonrpc.js +60 -0
- package/lib/sse.js +170 -0
- package/lib/stdio.js +149 -0
- package/lib/streamable-http.js +302 -0
- package/package.json +7 -8
package/lib/stdio.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stdio half of the bridge: newline delimited JSON-RPC over this process's
|
|
3
|
+
* stdin and stdout, which is the transport Claude Desktop and every other
|
|
4
|
+
* stdio only host speaks.
|
|
5
|
+
*
|
|
6
|
+
* This is a direct replacement for StdioServerTransport in
|
|
7
|
+
* @modelcontextprotocol/sdk, kept to the same framing and the same callback
|
|
8
|
+
* surface (onmessage, onerror, onclose) so bin/cli.js wires it up unchanged.
|
|
9
|
+
* It is here rather than imported because the SDK cannot ship that class on
|
|
10
|
+
* its own: taking it pulled in 90 packages and 22 MB, every one of them for
|
|
11
|
+
* the server and OAuth code a relay never reaches.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import process from 'node:process';
|
|
15
|
+
import { assertJsonRpcMessage } from './jsonrpc.js';
|
|
16
|
+
|
|
17
|
+
/** One line in, one frame out, throwing on anything that is not a frame. */
|
|
18
|
+
export function deserializeLine(line) {
|
|
19
|
+
return assertJsonRpcMessage(JSON.parse(line));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Ten megabytes, the SDK's ceiling. A line longer than this is not a frame. */
|
|
23
|
+
export const MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
/** Buffers a byte stream into whole lines, each of which is one JSON-RPC frame. */
|
|
26
|
+
export class ReadBuffer {
|
|
27
|
+
#buffer;
|
|
28
|
+
#maxBufferSize;
|
|
29
|
+
|
|
30
|
+
constructor({ maxBufferSize = MAX_BUFFER_SIZE } = {}) {
|
|
31
|
+
this.#maxBufferSize = maxBufferSize;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
append(chunk) {
|
|
35
|
+
const size = (this.#buffer?.length ?? 0) + chunk.length;
|
|
36
|
+
if (size > this.#maxBufferSize) {
|
|
37
|
+
this.clear();
|
|
38
|
+
throw new Error(`ReadBuffer exceeded maximum size of ${this.#maxBufferSize} bytes`);
|
|
39
|
+
}
|
|
40
|
+
this.#buffer = this.#buffer ? Buffer.concat([this.#buffer, chunk]) : chunk;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The next whole line as a parsed frame, or null while one is still arriving. */
|
|
44
|
+
readMessage(deserialize) {
|
|
45
|
+
if (!this.#buffer) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const newline = this.#buffer.indexOf('\n');
|
|
49
|
+
if (newline === -1) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const line = this.#buffer.toString('utf8', 0, newline).replace(/\r$/, '');
|
|
53
|
+
this.#buffer = this.#buffer.subarray(newline + 1);
|
|
54
|
+
return deserialize(line);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
clear() {
|
|
58
|
+
this.#buffer = undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class StdioServerTransport {
|
|
63
|
+
#stdin;
|
|
64
|
+
#stdout;
|
|
65
|
+
#readBuffer;
|
|
66
|
+
#started = false;
|
|
67
|
+
#deserialize;
|
|
68
|
+
|
|
69
|
+
onmessage;
|
|
70
|
+
onerror;
|
|
71
|
+
onclose;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param deserialize turns one line into a frame and throws on anything
|
|
75
|
+
* that is not one. Injected so the shape check lives in one place and the
|
|
76
|
+
* tests can drive the framing without it.
|
|
77
|
+
*/
|
|
78
|
+
constructor({ stdin = process.stdin, stdout = process.stdout, maxBufferSize, deserialize = deserializeLine } = {}) {
|
|
79
|
+
this.#stdin = stdin;
|
|
80
|
+
this.#stdout = stdout;
|
|
81
|
+
this.#readBuffer = new ReadBuffer({ maxBufferSize });
|
|
82
|
+
this.#deserialize = deserialize;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Arrow properties, so the same function identity comes off the emitter in
|
|
86
|
+
// close() as went on in start().
|
|
87
|
+
#ondata = (chunk) => {
|
|
88
|
+
try {
|
|
89
|
+
this.#readBuffer.append(chunk);
|
|
90
|
+
this.#drainReadBuffer();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.onerror?.(error);
|
|
93
|
+
this.close().catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
#onstdinerror = (error) => {
|
|
98
|
+
this.onerror?.(error);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
async start() {
|
|
102
|
+
if (this.#started) {
|
|
103
|
+
throw new Error('StdioServerTransport already started');
|
|
104
|
+
}
|
|
105
|
+
this.#started = true;
|
|
106
|
+
this.#stdin.on('data', this.#ondata);
|
|
107
|
+
this.#stdin.on('error', this.#onstdinerror);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A frame that will not parse is reported and skipped, never fatal: one bad
|
|
112
|
+
* line from the host must not take down a session whose next line is fine.
|
|
113
|
+
*/
|
|
114
|
+
#drainReadBuffer() {
|
|
115
|
+
while (true) {
|
|
116
|
+
try {
|
|
117
|
+
const message = this.#readBuffer.readMessage(this.#deserialize);
|
|
118
|
+
if (message === null) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.onmessage?.(message);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.onerror?.(error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async close() {
|
|
129
|
+
this.#stdin.off('data', this.#ondata);
|
|
130
|
+
this.#stdin.off('error', this.#onstdinerror);
|
|
131
|
+
// Only pause stdin if nothing else is reading it, so the bridge never
|
|
132
|
+
// stalls a host that shares the descriptor.
|
|
133
|
+
if (this.#stdin.listenerCount('data') === 0) {
|
|
134
|
+
this.#stdin.pause();
|
|
135
|
+
}
|
|
136
|
+
this.#readBuffer.clear();
|
|
137
|
+
this.onclose?.();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
send(message) {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
if (this.#stdout.write(`${JSON.stringify(message)}\n`)) {
|
|
143
|
+
resolve();
|
|
144
|
+
} else {
|
|
145
|
+
this.#stdout.once('drain', resolve);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP half of the bridge: MCP Streamable HTTP, client side.
|
|
3
|
+
*
|
|
4
|
+
* A direct replacement for StreamableHTTPClientTransport in
|
|
5
|
+
* @modelcontextprotocol/sdk, narrowed to what a relay does. It POSTs a frame,
|
|
6
|
+
* reads the answer as JSON or as an event stream, carries Mcp-Session-Id back
|
|
7
|
+
* on later requests, and opens the optional GET stream once the session is
|
|
8
|
+
* initialized. The SDK's OAuth client, its schema validation and its session
|
|
9
|
+
* termination are gone: the bridge authenticates with a bearer header it is
|
|
10
|
+
* handed, forwards frames without reading them, and is torn down by the host
|
|
11
|
+
* closing stdin.
|
|
12
|
+
*
|
|
13
|
+
* Headers are read from requestInit on every request rather than copied once,
|
|
14
|
+
* because the claim flow attaches Authorization to that same object minutes
|
|
15
|
+
* after the transport started, and the next request has to carry it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { assertJsonRpcPayload, expectsResponse } from './jsonrpc.js';
|
|
19
|
+
import { readEventStream } from './sse.js';
|
|
20
|
+
|
|
21
|
+
/** The SDK's reconnection defaults, kept so a resumable server sees no change. */
|
|
22
|
+
const RECONNECT = {
|
|
23
|
+
initialDelayMs: 1000,
|
|
24
|
+
maxDelayMs: 30_000,
|
|
25
|
+
growthFactor: 1.5,
|
|
26
|
+
maxRetries: 2,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class StreamableHttpError extends Error {
|
|
30
|
+
constructor(code, message) {
|
|
31
|
+
super(`Streamable HTTP error: ${message}`);
|
|
32
|
+
this.name = 'StreamableHttpError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** "application/json; charset=utf-8" is the same media type as "application/json". */
|
|
38
|
+
function mediaType(contentType) {
|
|
39
|
+
return (contentType ?? '').split(';')[0].trim().toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Headers arrive as an object, a Headers, or an array of pairs. */
|
|
43
|
+
function toPlainHeaders(headers) {
|
|
44
|
+
if (!headers) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
if (headers instanceof Headers) {
|
|
48
|
+
return Object.fromEntries(headers.entries());
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(headers)) {
|
|
51
|
+
return Object.fromEntries(headers);
|
|
52
|
+
}
|
|
53
|
+
return { ...headers };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Release a body we are not going to read, so the socket goes back to the pool. */
|
|
57
|
+
async function discard(response) {
|
|
58
|
+
try {
|
|
59
|
+
await response.body?.cancel();
|
|
60
|
+
} catch {
|
|
61
|
+
// A body already consumed or already errored needs nothing from us.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class StreamableHttpClientTransport {
|
|
66
|
+
#url;
|
|
67
|
+
#requestInit;
|
|
68
|
+
#fetch;
|
|
69
|
+
#sessionId;
|
|
70
|
+
#abortController;
|
|
71
|
+
#reconnectTimer;
|
|
72
|
+
#serverRetryMs;
|
|
73
|
+
|
|
74
|
+
onmessage;
|
|
75
|
+
onerror;
|
|
76
|
+
onclose;
|
|
77
|
+
|
|
78
|
+
constructor(url, { requestInit, fetch: fetchImpl = fetch, sessionId } = {}) {
|
|
79
|
+
this.#url = url;
|
|
80
|
+
this.#requestInit = requestInit;
|
|
81
|
+
this.#fetch = fetchImpl;
|
|
82
|
+
this.#sessionId = sessionId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get sessionId() {
|
|
86
|
+
return this.#sessionId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async start() {
|
|
90
|
+
if (this.#abortController) {
|
|
91
|
+
throw new Error('StreamableHttpClientTransport already started');
|
|
92
|
+
}
|
|
93
|
+
this.#abortController = new AbortController();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async close() {
|
|
97
|
+
if (this.#reconnectTimer) {
|
|
98
|
+
clearTimeout(this.#reconnectTimer);
|
|
99
|
+
this.#reconnectTimer = undefined;
|
|
100
|
+
}
|
|
101
|
+
this.#abortController?.abort();
|
|
102
|
+
this.onclose?.();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The session header first, then whatever the caller configured, so a header
|
|
107
|
+
* passed with --header or set by the claim flow wins over ours.
|
|
108
|
+
*/
|
|
109
|
+
#headers() {
|
|
110
|
+
const headers = new Headers();
|
|
111
|
+
if (this.#sessionId) {
|
|
112
|
+
headers.set('mcp-session-id', this.#sessionId);
|
|
113
|
+
}
|
|
114
|
+
for (const [name, value] of Object.entries(toPlainHeaders(this.#requestInit?.headers))) {
|
|
115
|
+
if (value !== undefined && value !== null) {
|
|
116
|
+
headers.set(name, String(value));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return headers;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async send(message) {
|
|
123
|
+
try {
|
|
124
|
+
const headers = this.#headers();
|
|
125
|
+
headers.set('content-type', 'application/json');
|
|
126
|
+
headers.set('accept', 'application/json, text/event-stream');
|
|
127
|
+
|
|
128
|
+
const response = await this.#fetch(this.#url, {
|
|
129
|
+
...this.#requestInit,
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers,
|
|
132
|
+
body: JSON.stringify(message),
|
|
133
|
+
signal: this.#abortController?.signal,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Stateful servers hand out a session on initialize and expect it back.
|
|
137
|
+
const sessionId = response.headers.get('mcp-session-id');
|
|
138
|
+
if (sessionId) {
|
|
139
|
+
this.#sessionId = sessionId;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const body = await response.text().catch(() => null);
|
|
144
|
+
throw new StreamableHttpError(response.status, `Error POSTing to endpoint: ${body}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (response.status === 202) {
|
|
148
|
+
await discard(response);
|
|
149
|
+
// The server accepted the notification and said nothing back. Once the
|
|
150
|
+
// session is initialized, that is the moment to try the GET stream a
|
|
151
|
+
// server may use to push messages. PreReason 405s it, which is fine.
|
|
152
|
+
if (!Array.isArray(message) && message?.method === 'notifications/initialized') {
|
|
153
|
+
this.#openServerStream({}).catch((error) => this.onerror?.(error));
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!expectsResponse(message)) {
|
|
159
|
+
// Nothing was asked, so nothing is read. The body still has to be
|
|
160
|
+
// released or the connection is held open until it times out.
|
|
161
|
+
await discard(response);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const type = mediaType(response.headers.get('content-type'));
|
|
166
|
+
if (type === 'application/json') {
|
|
167
|
+
this.#emit(await response.json());
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (type === 'text/event-stream') {
|
|
171
|
+
// Not awaited: the answer arrives through onmessage as the stream
|
|
172
|
+
// yields it, and send() should not stay pending for a tool call that
|
|
173
|
+
// streams for a minute.
|
|
174
|
+
this.#readStream(response, { reconnectable: false }).catch((error) => this.onerror?.(error));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await discard(response);
|
|
179
|
+
throw new StreamableHttpError(-1, `Unexpected content type: ${response.headers.get('content-type')}`);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.onerror?.(error);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** One JSON payload, one or many frames, each forwarded to the host. */
|
|
187
|
+
#emit(payload) {
|
|
188
|
+
let frames;
|
|
189
|
+
try {
|
|
190
|
+
frames = assertJsonRpcPayload(payload);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.onerror?.(error);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const frame of Array.isArray(frames) ? frames : [frames]) {
|
|
196
|
+
this.onmessage?.(frame);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read an event stream to the end, forwarding every message event.
|
|
202
|
+
*
|
|
203
|
+
* A frame that will not parse is reported and skipped rather than ending the
|
|
204
|
+
* stream, because the next event is usually fine.
|
|
205
|
+
*/
|
|
206
|
+
async #readStream(response, { reconnectable, lastEventId: startingEventId }) {
|
|
207
|
+
let lastEventId = startingEventId;
|
|
208
|
+
let sawResponse = false;
|
|
209
|
+
let sawEventId = false;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await readEventStream(response.body, {
|
|
213
|
+
onRetry: (retryMs) => {
|
|
214
|
+
this.#serverRetryMs = retryMs;
|
|
215
|
+
},
|
|
216
|
+
onEvent: (event) => {
|
|
217
|
+
if (event.id) {
|
|
218
|
+
lastEventId = event.id;
|
|
219
|
+
sawEventId = true;
|
|
220
|
+
}
|
|
221
|
+
if (event.event && event.event !== 'message') {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let frame;
|
|
225
|
+
try {
|
|
226
|
+
frame = assertJsonRpcPayload(JSON.parse(event.data));
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.onerror?.(error);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const one of Array.isArray(frame) ? frame : [frame]) {
|
|
232
|
+
if ('result' in one || 'error' in one) {
|
|
233
|
+
sawResponse = true;
|
|
234
|
+
}
|
|
235
|
+
this.onmessage?.(one);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.onerror?.(new Error(`SSE stream disconnected: ${error}`));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Reconnect the standalone GET stream, and a POST stream that carried an
|
|
244
|
+
// event id, which is the server saying where to resume from. Never once
|
|
245
|
+
// the answer has arrived: that request is finished, and reopening the
|
|
246
|
+
// stream would only replay it.
|
|
247
|
+
const canResume = reconnectable || sawEventId;
|
|
248
|
+
if (canResume && !sawResponse && this.#abortController && !this.#abortController.signal.aborted) {
|
|
249
|
+
this.#scheduleReconnect(lastEventId, 0);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#scheduleReconnect(lastEventId, attempt) {
|
|
254
|
+
if (attempt >= RECONNECT.maxRetries) {
|
|
255
|
+
this.onerror?.(new Error(`Maximum reconnection attempts (${RECONNECT.maxRetries}) exceeded.`));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const backoff = RECONNECT.initialDelayMs * RECONNECT.growthFactor ** attempt;
|
|
259
|
+
const delay = this.#serverRetryMs ?? Math.min(backoff, RECONNECT.maxDelayMs);
|
|
260
|
+
|
|
261
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
262
|
+
this.#openServerStream({ lastEventId }).catch((error) => {
|
|
263
|
+
this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`));
|
|
264
|
+
this.#scheduleReconnect(lastEventId, attempt + 1);
|
|
265
|
+
});
|
|
266
|
+
}, delay);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Open the optional GET stream a server may use to push messages.
|
|
271
|
+
*
|
|
272
|
+
* 405 is the documented way for a server to say it has no such stream, and
|
|
273
|
+
* PreReason's endpoint answers exactly that, so it is a quiet return and not
|
|
274
|
+
* an error.
|
|
275
|
+
*/
|
|
276
|
+
async #openServerStream({ lastEventId }) {
|
|
277
|
+
const headers = this.#headers();
|
|
278
|
+
headers.set('accept', 'text/event-stream');
|
|
279
|
+
if (lastEventId) {
|
|
280
|
+
headers.set('last-event-id', lastEventId);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const response = await this.#fetch(this.#url, {
|
|
284
|
+
...this.#requestInit,
|
|
285
|
+
method: 'GET',
|
|
286
|
+
headers,
|
|
287
|
+
signal: this.#abortController?.signal,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
await discard(response);
|
|
292
|
+
if (response.status === 405) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw new StreamableHttpError(response.status, `Failed to open SSE stream: ${response.statusText}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Not awaited: the stream runs for the life of the session while send()
|
|
299
|
+
// keeps working. Errors inside it go to onerror.
|
|
300
|
+
this.#readStream(response, { reconnectable: true, lastEventId }).catch((error) => this.onerror?.(error));
|
|
301
|
+
}
|
|
302
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prereason/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"mcpName": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"mcpName": "com.prereason/mcp",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Bitcoin and macro market briefings for AI agents: trend signals, regimes, liquidity and ETF flows.",
|
|
7
7
|
"bin": {
|
|
8
8
|
"prereason-mcp": "./bin/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "echo 'Build complete'"
|
|
11
|
+
"build": "echo 'Build complete'",
|
|
12
|
+
"test": "node --test test/credentials.test.js test/claim.test.js test/jsonrpc.test.js test/sse.test.js test/stdio.test.js test/streamable-http.test.js test/bridge.test.js"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"bin",
|
|
16
|
+
"lib",
|
|
15
17
|
"README.md",
|
|
16
18
|
"LICENSE",
|
|
17
19
|
"CHANGELOG.md",
|
|
18
20
|
".mcp.json"
|
|
19
21
|
],
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@modelcontextprotocol/sdk": "1.27.1"
|
|
22
|
-
},
|
|
23
22
|
"engines": {
|
|
24
23
|
"node": ">=18"
|
|
25
24
|
},
|
|
@@ -46,7 +45,7 @@
|
|
|
46
45
|
"url": "https://www.prereason.com"
|
|
47
46
|
},
|
|
48
47
|
"license": "MIT",
|
|
49
|
-
"homepage": "https://www.prereason.com/docs#mcp",
|
|
48
|
+
"homepage": "https://www.prereason.com/docs#mcp-integration",
|
|
50
49
|
"repository": {
|
|
51
50
|
"type": "git",
|
|
52
51
|
"url": "git+https://github.com/PreReason/mcp.git"
|