driggsby 0.0.1 → 0.1.2
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/LICENSE +8 -0
- package/README.md +65 -0
- package/dist/auth/browser.js +31 -0
- package/dist/auth/config.js +23 -0
- package/dist/auth/discovery.js +42 -0
- package/dist/auth/dpop.js +44 -0
- package/dist/auth/login.js +125 -0
- package/dist/auth/loopback.js +157 -0
- package/dist/auth/oauth.js +113 -0
- package/dist/auth/pkce.js +12 -0
- package/dist/auth/url-security.js +16 -0
- package/dist/broker/authentication.js +49 -0
- package/dist/broker/client.js +148 -0
- package/dist/broker/daemon.js +64 -0
- package/dist/broker/installation.js +142 -0
- package/dist/broker/ipc.js +12 -0
- package/dist/broker/launch.js +34 -0
- package/dist/broker/lock.js +84 -0
- package/dist/broker/remote-mcp.js +121 -0
- package/dist/broker/remote-session.js +103 -0
- package/dist/broker/secret-store.js +30 -0
- package/dist/broker/server.js +177 -0
- package/dist/broker/session.js +31 -0
- package/dist/broker/test-support.js +258 -0
- package/dist/broker/types.js +1 -0
- package/dist/cli/commands/broker-daemon.js +4 -0
- package/dist/cli/commands/login.js +31 -0
- package/dist/cli/commands/logout.js +16 -0
- package/dist/cli/commands/status.js +14 -0
- package/dist/cli/format.js +23 -0
- package/dist/index.js +39 -0
- package/dist/lib/json-file.js +36 -0
- package/dist/lib/runtime-paths.js +62 -0
- package/dist/shim/server.js +103 -0
- package/package.json +57 -3
- package/pyproject.toml +0 -11
- package/src/driggsby/__init__.py +0 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { fetchAuthorizationServerMetadata, } from "../auth/discovery.js";
|
|
2
|
+
import { createDpopProof } from "../auth/dpop.js";
|
|
3
|
+
import { refreshAccessToken } from "../auth/oauth.js";
|
|
4
|
+
import { assertBrokerRemoteUrl } from "../auth/url-security.js";
|
|
5
|
+
import { readBrokerDpopKeyPair } from "./installation.js";
|
|
6
|
+
import { readBrokerRemoteSession, summarizeBrokerRemoteSession, writeBrokerRemoteSession, } from "./session.js";
|
|
7
|
+
const ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
8
|
+
export class BrokerRemoteSessionManager {
|
|
9
|
+
brokerId;
|
|
10
|
+
fetchImpl;
|
|
11
|
+
runtimePaths;
|
|
12
|
+
secretStore;
|
|
13
|
+
refreshInFlight = null;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.brokerId = options.brokerId;
|
|
16
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
17
|
+
this.runtimePaths = options.runtimePaths;
|
|
18
|
+
this.secretStore = options.secretStore;
|
|
19
|
+
}
|
|
20
|
+
async ensureFreshSession() {
|
|
21
|
+
const session = await readBrokerRemoteSession(this.secretStore, this.brokerId);
|
|
22
|
+
if (session === null) {
|
|
23
|
+
throw new Error("The local Driggsby broker is not connected. Run `npx -y driggsby login` again.");
|
|
24
|
+
}
|
|
25
|
+
if (!sessionNeedsRefresh(session)) {
|
|
26
|
+
return session;
|
|
27
|
+
}
|
|
28
|
+
this.refreshInFlight ??= refreshRemoteSession(this.runtimePaths, this.secretStore, this.brokerId, session, this.fetchImpl).finally(() => {
|
|
29
|
+
this.refreshInFlight = null;
|
|
30
|
+
});
|
|
31
|
+
return await this.refreshInFlight;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function ensureFreshRemoteSession(options) {
|
|
35
|
+
const manager = new BrokerRemoteSessionManager(options);
|
|
36
|
+
return await manager.ensureFreshSession();
|
|
37
|
+
}
|
|
38
|
+
export async function inspectRemoteSessionReadiness(options) {
|
|
39
|
+
const session = await readBrokerRemoteSession(options.secretStore, options.brokerId);
|
|
40
|
+
if (session === null) {
|
|
41
|
+
return {
|
|
42
|
+
connected: false,
|
|
43
|
+
ready: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
connected: true,
|
|
48
|
+
ready: !sessionNeedsRefresh(session),
|
|
49
|
+
session: summarizeBrokerRemoteSession(session),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function sessionNeedsRefresh(session, nowMs = Date.now()) {
|
|
53
|
+
const expiresAtMs = Date.parse(session.accessTokenExpiresAt);
|
|
54
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return expiresAtMs - nowMs <= ACCESS_TOKEN_REFRESH_SKEW_MS;
|
|
58
|
+
}
|
|
59
|
+
async function refreshRemoteSession(runtimePaths, secretStore, brokerId, session, fetchImpl) {
|
|
60
|
+
assertBrokerRemoteUrl(session.issuer, "The Driggsby issuer URL");
|
|
61
|
+
let authorizationServerMetadata;
|
|
62
|
+
try {
|
|
63
|
+
authorizationServerMetadata = await fetchAuthorizationServerMetadata(session.issuer, fetchImpl);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error("Driggsby could not refresh the local broker session right now. Try again in a moment.");
|
|
67
|
+
}
|
|
68
|
+
let refreshedTokens;
|
|
69
|
+
try {
|
|
70
|
+
refreshedTokens = await refreshAccessToken({
|
|
71
|
+
clientId: session.clientId,
|
|
72
|
+
dpopProof: await refreshTokenDpopProof(runtimePaths, secretStore, brokerId, authorizationServerMetadata.token_endpoint),
|
|
73
|
+
metadata: authorizationServerMetadata,
|
|
74
|
+
refreshToken: session.refreshToken,
|
|
75
|
+
resource: session.resource,
|
|
76
|
+
}, fetchImpl);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error("The local Driggsby broker session expired. Run `npx -y driggsby login` again.");
|
|
80
|
+
}
|
|
81
|
+
const refreshedSession = {
|
|
82
|
+
...session,
|
|
83
|
+
accessToken: refreshedTokens.accessToken,
|
|
84
|
+
accessTokenExpiresAt: refreshedTokens.accessTokenExpiresAt,
|
|
85
|
+
refreshToken: refreshedTokens.refreshToken,
|
|
86
|
+
scope: refreshedTokens.scope,
|
|
87
|
+
tokenType: refreshedTokens.tokenType,
|
|
88
|
+
};
|
|
89
|
+
await writeBrokerRemoteSession(secretStore, brokerId, refreshedSession);
|
|
90
|
+
return refreshedSession;
|
|
91
|
+
}
|
|
92
|
+
async function refreshTokenDpopProof(runtimePaths, secretStore, brokerId, tokenEndpoint) {
|
|
93
|
+
const dpopKeyPair = await readBrokerDpopKeyPair(runtimePaths, secretStore, brokerId);
|
|
94
|
+
if (dpopKeyPair === null) {
|
|
95
|
+
throw new Error("The local Driggsby broker key is missing. Run `npx -y driggsby login` again.");
|
|
96
|
+
}
|
|
97
|
+
return await createDpopProof({
|
|
98
|
+
httpMethod: "POST",
|
|
99
|
+
privateJwk: dpopKeyPair.privateJwk,
|
|
100
|
+
publicJwk: dpopKeyPair.publicJwk,
|
|
101
|
+
targetUrl: tokenEndpoint,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AsyncEntry } from "@napi-rs/keyring";
|
|
2
|
+
export class KeyringSecretStore {
|
|
3
|
+
serviceName;
|
|
4
|
+
constructor(serviceName = "driggsby.local-broker") {
|
|
5
|
+
this.serviceName = serviceName;
|
|
6
|
+
}
|
|
7
|
+
async setSecret(account, secret) {
|
|
8
|
+
await new AsyncEntry(this.serviceName, account).setPassword(secret);
|
|
9
|
+
}
|
|
10
|
+
async getSecret(account) {
|
|
11
|
+
const secret = await new AsyncEntry(this.serviceName, account).getPassword();
|
|
12
|
+
return secret ?? null;
|
|
13
|
+
}
|
|
14
|
+
async deleteSecret(account) {
|
|
15
|
+
return await new AsyncEntry(this.serviceName, account).deleteCredential();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class MemorySecretStore {
|
|
19
|
+
secrets = new Map();
|
|
20
|
+
setSecret(account, secret) {
|
|
21
|
+
this.secrets.set(account, secret);
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
getSecret(account) {
|
|
25
|
+
return Promise.resolve(this.secrets.get(account) ?? null);
|
|
26
|
+
}
|
|
27
|
+
deleteSecret(account) {
|
|
28
|
+
return Promise.resolve(this.secrets.delete(account));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import { ensureRuntimeDirectories } from "../lib/runtime-paths.js";
|
|
4
|
+
import { acquireBrokerLock } from "./lock.js";
|
|
5
|
+
import { decodeRequest, encodeResponse } from "./ipc.js";
|
|
6
|
+
import { hashBrokerPayload, signBrokerProof } from "./authentication.js";
|
|
7
|
+
export class LocalBrokerServer {
|
|
8
|
+
brokerId;
|
|
9
|
+
localAuthToken;
|
|
10
|
+
privateJwk;
|
|
11
|
+
runtimePaths;
|
|
12
|
+
statusProvider;
|
|
13
|
+
listToolsProvider;
|
|
14
|
+
callToolProvider;
|
|
15
|
+
lockLease = null;
|
|
16
|
+
server = null;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.brokerId = options.brokerId;
|
|
19
|
+
this.localAuthToken = options.localAuthToken;
|
|
20
|
+
this.privateJwk = options.privateJwk;
|
|
21
|
+
this.runtimePaths = options.runtimePaths;
|
|
22
|
+
this.statusProvider = options.statusProvider;
|
|
23
|
+
this.listToolsProvider = options.listTools;
|
|
24
|
+
this.callToolProvider = options.callTool;
|
|
25
|
+
}
|
|
26
|
+
async listen() {
|
|
27
|
+
if (this.server !== null) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await ensureRuntimeDirectories(this.runtimePaths);
|
|
31
|
+
const lockLease = await acquireBrokerLock(this.runtimePaths.lockPath);
|
|
32
|
+
if (lockLease === null) {
|
|
33
|
+
throw new Error("The local Driggsby broker is already running.");
|
|
34
|
+
}
|
|
35
|
+
this.lockLease = lockLease;
|
|
36
|
+
if (process.platform !== "win32") {
|
|
37
|
+
await removeSocketIfPresent(this.runtimePaths.socketPath);
|
|
38
|
+
}
|
|
39
|
+
this.server = net.createServer((socket) => {
|
|
40
|
+
socket.setEncoding("utf8");
|
|
41
|
+
let buffer = "";
|
|
42
|
+
socket.on("data", (chunk) => {
|
|
43
|
+
buffer += chunk;
|
|
44
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
45
|
+
if (newlineIndex < 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const payload = buffer.slice(0, newlineIndex);
|
|
49
|
+
void this.handleRequest(payload, socket);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
await new Promise((resolve, reject) => {
|
|
54
|
+
this.server?.once("error", reject);
|
|
55
|
+
this.server?.listen(this.runtimePaths.socketPath, resolve);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
await this.releaseLock();
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (process.platform !== "win32") {
|
|
63
|
+
await fs.chmod(this.runtimePaths.socketPath, 0o600);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async close() {
|
|
67
|
+
if (this.server !== null) {
|
|
68
|
+
const server = this.server;
|
|
69
|
+
this.server = null;
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
server.close((error) => {
|
|
72
|
+
if (error) {
|
|
73
|
+
reject(error);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
resolve();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (process.platform !== "win32") {
|
|
81
|
+
await removeSocketIfPresent(this.runtimePaths.socketPath);
|
|
82
|
+
}
|
|
83
|
+
await this.releaseLock();
|
|
84
|
+
}
|
|
85
|
+
async handleRequest(payload, socket) {
|
|
86
|
+
let response;
|
|
87
|
+
try {
|
|
88
|
+
const request = decodeRequest(payload);
|
|
89
|
+
response = await this.dispatchRequest(request);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
response = await this.buildErrorResponse("unknown", "unknown", "unknown", error instanceof Error ? error.message : "Broker request failed.");
|
|
93
|
+
}
|
|
94
|
+
socket.end(encodeResponse(response));
|
|
95
|
+
}
|
|
96
|
+
async dispatchRequest(request) {
|
|
97
|
+
if (request.authToken !== this.localAuthToken) {
|
|
98
|
+
return await this.buildErrorResponse(request.id, request.method, request.challenge, "Broker authentication failed.");
|
|
99
|
+
}
|
|
100
|
+
switch (request.method) {
|
|
101
|
+
case "ping": {
|
|
102
|
+
const status = await this.statusProvider();
|
|
103
|
+
return await this.buildSuccessResponse(request, {
|
|
104
|
+
ok: true,
|
|
105
|
+
brokerId: status.brokerId ?? "unknown",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
case "get_status":
|
|
109
|
+
return await this.buildSuccessResponse(request, {
|
|
110
|
+
status: await this.statusProvider(),
|
|
111
|
+
});
|
|
112
|
+
case "shutdown":
|
|
113
|
+
setImmediate(() => {
|
|
114
|
+
void this.close();
|
|
115
|
+
});
|
|
116
|
+
return await this.buildSuccessResponse(request, {
|
|
117
|
+
stopped: true,
|
|
118
|
+
});
|
|
119
|
+
case "list_tools":
|
|
120
|
+
return await this.buildSuccessResponse(request, await this.listToolsProvider());
|
|
121
|
+
case "call_tool":
|
|
122
|
+
return await this.buildSuccessResponse(request, await this.callToolProvider(request.toolName, request.args));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async buildSuccessResponse(request, result) {
|
|
126
|
+
const payload = { ok: true, result };
|
|
127
|
+
return {
|
|
128
|
+
brokerProof: await signBrokerProof({
|
|
129
|
+
aud: "driggsby-local-shim",
|
|
130
|
+
challenge: request.challenge,
|
|
131
|
+
payloadSha256: hashBrokerPayload(payload),
|
|
132
|
+
requestId: request.id,
|
|
133
|
+
requestMethod: request.method,
|
|
134
|
+
sub: this.brokerId,
|
|
135
|
+
}, this.privateJwk),
|
|
136
|
+
id: request.id,
|
|
137
|
+
ok: true,
|
|
138
|
+
result,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async buildErrorResponse(requestId, requestMethod, challenge, error) {
|
|
142
|
+
const payload = { ok: false, error };
|
|
143
|
+
return {
|
|
144
|
+
brokerProof: await signBrokerProof({
|
|
145
|
+
aud: "driggsby-local-shim",
|
|
146
|
+
challenge,
|
|
147
|
+
payloadSha256: hashBrokerPayload(payload),
|
|
148
|
+
requestId,
|
|
149
|
+
requestMethod: requestMethod === "unknown" ? "ping" : requestMethod,
|
|
150
|
+
sub: this.brokerId,
|
|
151
|
+
}, this.privateJwk),
|
|
152
|
+
id: requestId,
|
|
153
|
+
ok: false,
|
|
154
|
+
error,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async releaseLock() {
|
|
158
|
+
if (this.lockLease === null) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const lease = this.lockLease;
|
|
162
|
+
this.lockLease = null;
|
|
163
|
+
await lease.release();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function removeSocketIfPresent(socketPath) {
|
|
167
|
+
try {
|
|
168
|
+
await fs.rm(socketPath);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!(error instanceof Error) ||
|
|
172
|
+
!("code" in error) ||
|
|
173
|
+
error.code !== "ENOENT") {
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const REMOTE_SESSION_ACCOUNT_SUFFIX = "remote-session";
|
|
2
|
+
export async function readBrokerRemoteSession(secretStore, brokerId) {
|
|
3
|
+
const raw = await secretStore.getSecret(remoteSessionAccountName(brokerId));
|
|
4
|
+
if (raw === null) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
return {
|
|
9
|
+
...parsed,
|
|
10
|
+
tokenType: parsed.tokenType ?? "Bearer",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export async function writeBrokerRemoteSession(secretStore, brokerId, session) {
|
|
14
|
+
await secretStore.setSecret(remoteSessionAccountName(brokerId), JSON.stringify(session));
|
|
15
|
+
}
|
|
16
|
+
export async function clearBrokerRemoteSession(secretStore, brokerId) {
|
|
17
|
+
await secretStore.deleteSecret(remoteSessionAccountName(brokerId));
|
|
18
|
+
}
|
|
19
|
+
export function summarizeBrokerRemoteSession(session) {
|
|
20
|
+
return {
|
|
21
|
+
accessTokenExpiresAt: session.accessTokenExpiresAt,
|
|
22
|
+
authenticatedAt: session.authenticatedAt,
|
|
23
|
+
clientId: session.clientId,
|
|
24
|
+
issuer: session.issuer,
|
|
25
|
+
resource: session.resource,
|
|
26
|
+
scope: session.scope,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function remoteSessionAccountName(brokerId) {
|
|
30
|
+
return `${brokerId}:${REMOTE_SESSION_ACCOUNT_SUFFIX}`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp } from "node:fs/promises";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { decodeProtectedHeader, importJWK, jwtVerify, } from "jose";
|
|
8
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
export async function makeBrokerRuntimePaths(prefix) {
|
|
12
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
13
|
+
return {
|
|
14
|
+
configDir: path.join(baseDir, "config"),
|
|
15
|
+
stateDir: path.join(baseDir, "state"),
|
|
16
|
+
metadataPath: path.join(baseDir, "config", "broker-metadata.json"),
|
|
17
|
+
lockPath: path.join(baseDir, "state", "broker.lock"),
|
|
18
|
+
socketPath: path.join(baseDir, "state", "broker.sock"),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function startFakeRemoteService() {
|
|
22
|
+
let currentAccessToken = "refreshed-access-token";
|
|
23
|
+
let tokenRequestCount = 0;
|
|
24
|
+
let listToolsCount = 0;
|
|
25
|
+
let callToolCount = 0;
|
|
26
|
+
let exposeSavingsTool = false;
|
|
27
|
+
const server = http.createServer((request, response) => {
|
|
28
|
+
void handleRequest(request, response);
|
|
29
|
+
});
|
|
30
|
+
async function handleRequest(request, response) {
|
|
31
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
32
|
+
if (request.method === "GET" &&
|
|
33
|
+
requestUrl.pathname === "/.well-known/oauth-authorization-server") {
|
|
34
|
+
response.setHeader("content-type", "application/json");
|
|
35
|
+
response.end(JSON.stringify({
|
|
36
|
+
issuer: originForServer(server),
|
|
37
|
+
authorization_endpoint: `${originForServer(server)}/authorize`,
|
|
38
|
+
token_endpoint: `${originForServer(server)}/token`,
|
|
39
|
+
registration_endpoint: `${originForServer(server)}/register`,
|
|
40
|
+
response_types_supported: ["code"],
|
|
41
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
42
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
43
|
+
dpop_signing_alg_values_supported: ["ES256"],
|
|
44
|
+
code_challenge_methods_supported: ["S256"],
|
|
45
|
+
scopes_supported: ["driggsby.read"],
|
|
46
|
+
}));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (request.method === "POST" && requestUrl.pathname === "/token") {
|
|
50
|
+
tokenRequestCount += 1;
|
|
51
|
+
const body = await readRequestBody(request);
|
|
52
|
+
const params = new URLSearchParams(body);
|
|
53
|
+
assert.equal(params.get("client_id"), "client-123");
|
|
54
|
+
assert.equal(params.get("grant_type"), "refresh_token");
|
|
55
|
+
assert.equal(params.get("refresh_token"), "refresh-token-1");
|
|
56
|
+
assert.equal(params.get("resource"), `${originForServer(server)}/mcp`);
|
|
57
|
+
await assertDpopProof(request.headers["dpop"], {
|
|
58
|
+
authorizationHeader: undefined,
|
|
59
|
+
expectedMethod: "POST",
|
|
60
|
+
expectedUrl: `${originForServer(server)}/token`,
|
|
61
|
+
});
|
|
62
|
+
currentAccessToken = "refreshed-access-token";
|
|
63
|
+
response.setHeader("content-type", "application/json");
|
|
64
|
+
response.end(JSON.stringify({
|
|
65
|
+
access_token: currentAccessToken,
|
|
66
|
+
token_type: "DPoP",
|
|
67
|
+
expires_in: 3600,
|
|
68
|
+
refresh_token: "refresh-token-2",
|
|
69
|
+
scope: "driggsby.read",
|
|
70
|
+
}));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (requestUrl.pathname === "/mcp" && request.method === "GET") {
|
|
74
|
+
response.statusCode = 405;
|
|
75
|
+
response.setHeader("content-type", "application/json");
|
|
76
|
+
response.end(JSON.stringify({
|
|
77
|
+
error: {
|
|
78
|
+
code: -32000,
|
|
79
|
+
message: "Method not allowed.",
|
|
80
|
+
},
|
|
81
|
+
id: null,
|
|
82
|
+
jsonrpc: "2.0",
|
|
83
|
+
}));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (requestUrl.pathname === "/mcp" && request.method === "POST") {
|
|
87
|
+
if (request.headers.authorization !== `DPoP ${currentAccessToken}`) {
|
|
88
|
+
response.statusCode = 401;
|
|
89
|
+
response.end();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await assertDpopProof(request.headers["dpop"], {
|
|
93
|
+
authorizationHeader: request.headers.authorization,
|
|
94
|
+
expectedMethod: "POST",
|
|
95
|
+
expectedUrl: `${originForServer(server)}/mcp`,
|
|
96
|
+
});
|
|
97
|
+
const mcpServer = new Server({
|
|
98
|
+
name: "fake-remote-mcp",
|
|
99
|
+
version: "0.1.0",
|
|
100
|
+
}, {
|
|
101
|
+
capabilities: {
|
|
102
|
+
tools: {},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
mcpServer.setRequestHandler(ListToolsRequestSchema, () => {
|
|
106
|
+
listToolsCount += 1;
|
|
107
|
+
return {
|
|
108
|
+
tools: [
|
|
109
|
+
{
|
|
110
|
+
description: "Echo a balance amount back to the caller.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
properties: {
|
|
114
|
+
amount: {
|
|
115
|
+
type: "string",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ["amount"],
|
|
119
|
+
type: "object",
|
|
120
|
+
},
|
|
121
|
+
name: "echo_balance",
|
|
122
|
+
},
|
|
123
|
+
...(exposeSavingsTool
|
|
124
|
+
? [
|
|
125
|
+
{
|
|
126
|
+
description: "Project a savings target over time.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
additionalProperties: false,
|
|
129
|
+
properties: {},
|
|
130
|
+
type: "object",
|
|
131
|
+
},
|
|
132
|
+
name: "savings_projection",
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
: []),
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
mcpServer.setRequestHandler(CallToolRequestSchema, (mcpRequest) => {
|
|
140
|
+
callToolCount += 1;
|
|
141
|
+
const amount = readAmountArgument(mcpRequest.params.arguments);
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
text: `echoed ${amount}`,
|
|
146
|
+
type: "text",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
const transport = new StreamableHTTPServerTransport({});
|
|
152
|
+
await mcpServer.connect(toTransport(transport));
|
|
153
|
+
await transport.handleRequest(request, response, await readJsonBody(request));
|
|
154
|
+
response.once("close", () => {
|
|
155
|
+
void transport.close();
|
|
156
|
+
void mcpServer.close();
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
response.statusCode = 404;
|
|
161
|
+
response.end();
|
|
162
|
+
}
|
|
163
|
+
await new Promise((resolve, reject) => {
|
|
164
|
+
server.once("error", reject);
|
|
165
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
async close() {
|
|
169
|
+
await new Promise((resolve, reject) => {
|
|
170
|
+
server.close((error) => {
|
|
171
|
+
if (error) {
|
|
172
|
+
reject(error);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
resolve();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
get callToolCount() {
|
|
180
|
+
return callToolCount;
|
|
181
|
+
},
|
|
182
|
+
get listToolsCount() {
|
|
183
|
+
return listToolsCount;
|
|
184
|
+
},
|
|
185
|
+
get origin() {
|
|
186
|
+
return originForServer(server);
|
|
187
|
+
},
|
|
188
|
+
setExposeSavingsTool(value) {
|
|
189
|
+
exposeSavingsTool = value;
|
|
190
|
+
},
|
|
191
|
+
get tokenRequestCount() {
|
|
192
|
+
return tokenRequestCount;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function readAmountArgument(argumentsValue) {
|
|
197
|
+
if (typeof argumentsValue !== "object" ||
|
|
198
|
+
argumentsValue === null ||
|
|
199
|
+
Array.isArray(argumentsValue)) {
|
|
200
|
+
throw new Error("Expected object arguments.");
|
|
201
|
+
}
|
|
202
|
+
const amount = argumentsValue["amount"];
|
|
203
|
+
if (typeof amount !== "string") {
|
|
204
|
+
throw new Error("Expected a string amount argument.");
|
|
205
|
+
}
|
|
206
|
+
return amount;
|
|
207
|
+
}
|
|
208
|
+
async function readJsonBody(request) {
|
|
209
|
+
const body = await readRequestBody(request);
|
|
210
|
+
if (body.length === 0) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return JSON.parse(body);
|
|
214
|
+
}
|
|
215
|
+
async function assertDpopProof(proof, options) {
|
|
216
|
+
if (typeof proof !== "string") {
|
|
217
|
+
throw new Error("Expected a DPoP proof header.");
|
|
218
|
+
}
|
|
219
|
+
const protectedHeader = decodeProtectedHeader(proof);
|
|
220
|
+
const verificationKey = await importJWK(protectedHeader.jwk, "ES256");
|
|
221
|
+
const { payload } = await jwtVerify(proof, verificationKey, {
|
|
222
|
+
algorithms: ["ES256"],
|
|
223
|
+
typ: "dpop+jwt",
|
|
224
|
+
});
|
|
225
|
+
assert.equal(payload["htm"], options.expectedMethod);
|
|
226
|
+
assert.equal(payload["htu"], options.expectedUrl);
|
|
227
|
+
if (options.authorizationHeader !== undefined) {
|
|
228
|
+
const [, accessToken] = options.authorizationHeader.split(" ");
|
|
229
|
+
if (accessToken === undefined) {
|
|
230
|
+
throw new Error("Expected an access token in the authorization header.");
|
|
231
|
+
}
|
|
232
|
+
assert.ok(typeof payload["ath"] === "string");
|
|
233
|
+
assert.equal(payload["ath"], await sha256Base64Url(accessToken));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function sha256Base64Url(value) {
|
|
237
|
+
return (await import("node:crypto"))
|
|
238
|
+
.createHash("sha256")
|
|
239
|
+
.update(value)
|
|
240
|
+
.digest("base64url");
|
|
241
|
+
}
|
|
242
|
+
async function readRequestBody(request) {
|
|
243
|
+
let body = "";
|
|
244
|
+
for await (const chunk of request) {
|
|
245
|
+
body += String(chunk);
|
|
246
|
+
}
|
|
247
|
+
return body;
|
|
248
|
+
}
|
|
249
|
+
function originForServer(server) {
|
|
250
|
+
const address = server.address();
|
|
251
|
+
if (address === null || typeof address === "string") {
|
|
252
|
+
throw new Error("Expected an HTTP server address.");
|
|
253
|
+
}
|
|
254
|
+
return `http://127.0.0.1:${address.port}`;
|
|
255
|
+
}
|
|
256
|
+
function toTransport(transport) {
|
|
257
|
+
return transport;
|
|
258
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { loginBroker } from "../../auth/login.js";
|
|
2
|
+
import { KeyringSecretStore } from "../../broker/secret-store.js";
|
|
3
|
+
import { ensureRuntimeDirectories } from "../../lib/runtime-paths.js";
|
|
4
|
+
export async function runLoginCommand(runtimePaths) {
|
|
5
|
+
await ensureRuntimeDirectories(runtimePaths);
|
|
6
|
+
const secretStore = new KeyringSecretStore();
|
|
7
|
+
process.stdout.write("Opening Driggsby sign-in in your browser...\n");
|
|
8
|
+
const result = await loginBroker(runtimePaths, {
|
|
9
|
+
onBrowserPrompt: ({ browserOpened, signInUrl }) => {
|
|
10
|
+
if (!browserOpened) {
|
|
11
|
+
process.stdout.write([
|
|
12
|
+
"Your browser did not open automatically.",
|
|
13
|
+
"Open this URL to finish connecting Driggsby:",
|
|
14
|
+
signInUrl,
|
|
15
|
+
"",
|
|
16
|
+
].join("\n"));
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
secretStore,
|
|
20
|
+
});
|
|
21
|
+
process.stdout.write([
|
|
22
|
+
"Local Driggsby broker is connected.",
|
|
23
|
+
`broker id: ${result.brokerId}`,
|
|
24
|
+
`dpop thumbprint: ${result.dpopThumbprint}`,
|
|
25
|
+
`server: ${result.session.issuer}`,
|
|
26
|
+
`resource: ${result.session.resource}`,
|
|
27
|
+
`scope: ${result.session.scope}`,
|
|
28
|
+
`access token expires: ${result.session.accessTokenExpiresAt}`,
|
|
29
|
+
"",
|
|
30
|
+
].join("\n"));
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getBrokerStatus, shutdownBroker } from "../../broker/client.js";
|
|
2
|
+
import { clearBrokerInstallation } from "../../broker/installation.js";
|
|
3
|
+
import { KeyringSecretStore } from "../../broker/secret-store.js";
|
|
4
|
+
export async function runLogoutCommand(runtimePaths) {
|
|
5
|
+
const secretStore = new KeyringSecretStore();
|
|
6
|
+
const clientOptions = {
|
|
7
|
+
runtimePaths,
|
|
8
|
+
secretStore,
|
|
9
|
+
};
|
|
10
|
+
const runningStatus = await getBrokerStatus(clientOptions);
|
|
11
|
+
if (runningStatus !== null && !(await shutdownBroker(clientOptions))) {
|
|
12
|
+
throw new Error("The local Driggsby broker did not shut down cleanly. Close active MCP sessions and try again.");
|
|
13
|
+
}
|
|
14
|
+
await clearBrokerInstallation(runtimePaths, secretStore);
|
|
15
|
+
process.stdout.write("Local Driggsby broker state cleared.\n");
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getBrokerStatus } from "../../broker/client.js";
|
|
2
|
+
import { buildBrokerStatus } from "../../broker/installation.js";
|
|
3
|
+
import { KeyringSecretStore } from "../../broker/secret-store.js";
|
|
4
|
+
import { formatStatusText } from "../format.js";
|
|
5
|
+
export async function runStatusCommand(runtimePaths) {
|
|
6
|
+
const secretStore = new KeyringSecretStore();
|
|
7
|
+
const liveStatus = await getBrokerStatus({
|
|
8
|
+
runtimePaths,
|
|
9
|
+
secretStore,
|
|
10
|
+
});
|
|
11
|
+
const status = liveStatus ??
|
|
12
|
+
(await buildBrokerStatus(runtimePaths, secretStore, false));
|
|
13
|
+
process.stdout.write(formatStatusText(status));
|
|
14
|
+
}
|