@signet-auth/mcp 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 @@
|
|
|
1
|
+
export { SigningTransport, type SigningTransportOptions, type Transport, type JSONRPCMessage } from './signing-transport.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SigningTransport } from './signing-transport.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type SignetReceipt } from '@signet-auth/core';
|
|
2
|
+
export interface JSONRPCMessage {
|
|
3
|
+
jsonrpc: '2.0';
|
|
4
|
+
id?: string | number;
|
|
5
|
+
method?: string;
|
|
6
|
+
params?: Record<string, unknown>;
|
|
7
|
+
result?: unknown;
|
|
8
|
+
error?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface Transport {
|
|
11
|
+
start(): Promise<void>;
|
|
12
|
+
send(message: JSONRPCMessage, options?: unknown): Promise<void>;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
onclose?: () => void;
|
|
15
|
+
onerror?: (error: Error) => void;
|
|
16
|
+
onmessage?: (message: JSONRPCMessage, extra?: unknown) => void;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
setProtocolVersion?: (version: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface SigningTransportOptions {
|
|
21
|
+
target?: string;
|
|
22
|
+
transport?: string;
|
|
23
|
+
onSign?: (receipt: SignetReceipt) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare class SigningTransport implements Transport {
|
|
26
|
+
private inner;
|
|
27
|
+
private secretKey;
|
|
28
|
+
private signerName;
|
|
29
|
+
private signerOwner;
|
|
30
|
+
private opts;
|
|
31
|
+
constructor(inner: Transport, secretKey: string, signerName: string, signerOwner?: string, options?: SigningTransportOptions);
|
|
32
|
+
onclose?: () => void;
|
|
33
|
+
onerror?: (error: Error) => void;
|
|
34
|
+
onmessage?: (message: JSONRPCMessage, extra?: unknown) => void;
|
|
35
|
+
get sessionId(): string | undefined;
|
|
36
|
+
setProtocolVersion: (v: string) => void;
|
|
37
|
+
start(): Promise<void>;
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
send(message: JSONRPCMessage, options?: unknown): Promise<void>;
|
|
40
|
+
private isToolCall;
|
|
41
|
+
private signToolCall;
|
|
42
|
+
private injectSignet;
|
|
43
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { sign } from '@signet-auth/core';
|
|
2
|
+
export class SigningTransport {
|
|
3
|
+
inner;
|
|
4
|
+
secretKey;
|
|
5
|
+
signerName;
|
|
6
|
+
signerOwner;
|
|
7
|
+
opts;
|
|
8
|
+
constructor(inner, secretKey, signerName, signerOwner, options) {
|
|
9
|
+
this.inner = inner;
|
|
10
|
+
this.secretKey = secretKey;
|
|
11
|
+
this.signerName = signerName;
|
|
12
|
+
this.signerOwner = signerOwner ?? '';
|
|
13
|
+
this.opts = options ?? {};
|
|
14
|
+
// Forward callbacks using lazy closures.
|
|
15
|
+
// MCP SDK's Protocol.connect() sets our callbacks AFTER construction,
|
|
16
|
+
// so these closures must read this.onclose/etc lazily at call time.
|
|
17
|
+
this.inner.onclose = () => this.onclose?.();
|
|
18
|
+
this.inner.onerror = (e) => this.onerror?.(e);
|
|
19
|
+
this.inner.onmessage = (msg, extra) => this.onmessage?.(msg, extra);
|
|
20
|
+
}
|
|
21
|
+
onclose;
|
|
22
|
+
onerror;
|
|
23
|
+
onmessage;
|
|
24
|
+
get sessionId() { return this.inner.sessionId; }
|
|
25
|
+
setProtocolVersion = (v) => {
|
|
26
|
+
this.inner.setProtocolVersion?.(v);
|
|
27
|
+
};
|
|
28
|
+
start() { return this.inner.start(); }
|
|
29
|
+
close() { return this.inner.close(); }
|
|
30
|
+
async send(message, options) {
|
|
31
|
+
if (this.isToolCall(message)) {
|
|
32
|
+
const receipt = this.signToolCall(message);
|
|
33
|
+
this.injectSignet(message, receipt);
|
|
34
|
+
this.opts.onSign?.(receipt);
|
|
35
|
+
}
|
|
36
|
+
return this.inner.send(message, options);
|
|
37
|
+
}
|
|
38
|
+
isToolCall(message) {
|
|
39
|
+
return message.method === 'tools/call';
|
|
40
|
+
}
|
|
41
|
+
signToolCall(message) {
|
|
42
|
+
const params = (message.params ?? {});
|
|
43
|
+
const action = {
|
|
44
|
+
tool: params.name ?? 'unknown',
|
|
45
|
+
params: params.arguments ?? {},
|
|
46
|
+
params_hash: '',
|
|
47
|
+
target: this.opts.target ?? 'unknown',
|
|
48
|
+
transport: this.opts.transport ?? 'stdio',
|
|
49
|
+
};
|
|
50
|
+
return sign(this.secretKey, action, this.signerName, this.signerOwner);
|
|
51
|
+
}
|
|
52
|
+
injectSignet(message, receipt) {
|
|
53
|
+
// Deep-clone params to avoid mutating the original object
|
|
54
|
+
const params = JSON.parse(JSON.stringify(message.params ?? {}));
|
|
55
|
+
if (!params._meta)
|
|
56
|
+
params._meta = {};
|
|
57
|
+
params._meta._signet = {
|
|
58
|
+
...receipt,
|
|
59
|
+
action: { ...receipt.action, params: null },
|
|
60
|
+
};
|
|
61
|
+
message.params = params;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { generateKeypair } from '@signet-auth/core';
|
|
4
|
+
import { SigningTransport } from '../src/index.js';
|
|
5
|
+
// Mock transport that records sent messages
|
|
6
|
+
class MockTransport {
|
|
7
|
+
sent = [];
|
|
8
|
+
onclose;
|
|
9
|
+
onerror;
|
|
10
|
+
onmessage;
|
|
11
|
+
sessionId;
|
|
12
|
+
async start() { }
|
|
13
|
+
async close() { }
|
|
14
|
+
async send(message) {
|
|
15
|
+
this.sent.push(JSON.parse(JSON.stringify(message)));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
describe('@signet/mcp SigningTransport', () => {
|
|
19
|
+
const kp = generateKeypair();
|
|
20
|
+
function createTransport() {
|
|
21
|
+
const mock = new MockTransport();
|
|
22
|
+
const signing = new SigningTransport(mock, kp.secretKey, 'test-agent', 'owner', {
|
|
23
|
+
target: 'mcp://test-server',
|
|
24
|
+
transport: 'stdio',
|
|
25
|
+
});
|
|
26
|
+
return { mock, signing };
|
|
27
|
+
}
|
|
28
|
+
function toolCallMessage(name, args) {
|
|
29
|
+
return {
|
|
30
|
+
jsonrpc: '2.0',
|
|
31
|
+
id: 1,
|
|
32
|
+
method: 'tools/call',
|
|
33
|
+
params: { name, arguments: args },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
it('signs tool call messages', async () => {
|
|
37
|
+
const { mock, signing } = createTransport();
|
|
38
|
+
await signing.send(toolCallMessage('echo', { message: 'hello' }));
|
|
39
|
+
assert.strictEqual(mock.sent.length, 1);
|
|
40
|
+
const sent = mock.sent[0];
|
|
41
|
+
assert(sent.params._meta._signet, '_signet should be injected');
|
|
42
|
+
assert(sent.params._meta._signet.sig.startsWith('ed25519:'));
|
|
43
|
+
assert(sent.params._meta._signet.id.startsWith('rec_'));
|
|
44
|
+
});
|
|
45
|
+
it('passes through non-tool-call messages', async () => {
|
|
46
|
+
const { mock, signing } = createTransport();
|
|
47
|
+
const listMsg = { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} };
|
|
48
|
+
await signing.send(listMsg);
|
|
49
|
+
assert.strictEqual(mock.sent.length, 1);
|
|
50
|
+
const sent = mock.sent[0];
|
|
51
|
+
assert.strictEqual(sent.params._meta, undefined);
|
|
52
|
+
});
|
|
53
|
+
it('receipt has correct tool name', async () => {
|
|
54
|
+
const { mock, signing } = createTransport();
|
|
55
|
+
await signing.send(toolCallMessage('github_create_issue', { title: 'bug' }));
|
|
56
|
+
const signet = mock.sent[0].params._meta._signet;
|
|
57
|
+
assert.strictEqual(signet.action.tool, 'github_create_issue');
|
|
58
|
+
});
|
|
59
|
+
it('receipt params are null (hash-only)', async () => {
|
|
60
|
+
const { mock, signing } = createTransport();
|
|
61
|
+
await signing.send(toolCallMessage('echo', { data: 'secret' }));
|
|
62
|
+
const signet = mock.sent[0].params._meta._signet;
|
|
63
|
+
assert.strictEqual(signet.action.params, null);
|
|
64
|
+
assert(signet.action.params_hash.startsWith('sha256:'));
|
|
65
|
+
});
|
|
66
|
+
it('onSign callback fires with receipt', async () => {
|
|
67
|
+
const mock = new MockTransport();
|
|
68
|
+
let callbackReceipt = null;
|
|
69
|
+
const signing = new SigningTransport(mock, kp.secretKey, 'test-agent', 'owner', {
|
|
70
|
+
onSign: (r) => { callbackReceipt = r; },
|
|
71
|
+
});
|
|
72
|
+
await signing.send(toolCallMessage('test', {}));
|
|
73
|
+
assert(callbackReceipt !== null, 'callback should have been called');
|
|
74
|
+
const r = callbackReceipt;
|
|
75
|
+
assert(r.id.startsWith('rec_'));
|
|
76
|
+
assert.strictEqual(r.signer.name, 'test-agent');
|
|
77
|
+
});
|
|
78
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@signet-auth/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP middleware for Signet cryptographic action receipts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
|
+
"types": "dist/src/index.d.ts",
|
|
8
|
+
"files": ["dist/"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "npx tsc",
|
|
11
|
+
"test": "npx tsc && node --test dist/tests/mcp.test.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@signet-auth/core": "file:../signet-core"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22",
|
|
18
|
+
"typescript": "^5"
|
|
19
|
+
},
|
|
20
|
+
"license": "Apache-2.0 OR MIT"
|
|
21
|
+
}
|