driggsby 0.1.7 → 0.1.9

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.
@@ -4,6 +4,7 @@ import { generateDpopKeyMaterial } from "../auth/dpop.js";
4
4
  import { readJsonFile, removeFileIfPresent, writeJsonFile } from "../lib/json-file.js";
5
5
  import { generateLocalAuthToken } from "./authentication.js";
6
6
  import { inspectRemoteSessionReadiness } from "./remote-session.js";
7
+ import { buildNotConnectedReadiness } from "./remote-session.js";
7
8
  import { clearBrokerRemoteSession, readBrokerRemoteSession, } from "./session.js";
8
9
  const LOCAL_AUTH_TOKEN_ACCOUNT_SUFFIX = "local-auth-token";
9
10
  const PRIVATE_KEY_ACCOUNT_SUFFIX = "dpop-private-jwk";
@@ -57,34 +58,40 @@ export async function inspectBrokerReadiness(runtimePaths, secretStore) {
57
58
  export async function buildBrokerStatus(runtimePaths, secretStore, brokerRunning) {
58
59
  const readiness = await inspectBrokerReadiness(runtimePaths, secretStore);
59
60
  const remoteReadiness = readiness.brokerId === undefined
60
- ? {
61
- connected: false,
62
- ready: false,
63
- }
61
+ ? buildNotConnectedReadiness()
64
62
  : await inspectRemoteSessionReadiness({
65
63
  brokerId: readiness.brokerId,
64
+ refreshIfNeeded: true,
66
65
  runtimePaths,
67
66
  secretStore,
68
67
  });
69
68
  const status = {
70
69
  installed: readiness.installed && readiness.privateKeyPresent,
71
70
  brokerRunning,
71
+ ...(remoteReadiness.nextStepCommand === undefined
72
+ ? {}
73
+ : { nextStepCommand: remoteReadiness.nextStepCommand }),
74
+ remoteAccessDetail: remoteReadiness.detail,
75
+ remoteAccessState: remoteReadiness.state,
72
76
  remoteMcpReady: remoteReadiness.ready,
73
77
  socketPath: runtimePaths.socketPath,
78
+ ...(readiness.brokerId === undefined ? {} : { brokerId: readiness.brokerId }),
79
+ ...(readiness.dpopThumbprint === undefined
80
+ ? {}
81
+ : { dpopThumbprint: readiness.dpopThumbprint }),
82
+ ...(remoteReadiness.session === undefined
83
+ ? {}
84
+ : { remoteSession: remoteReadiness.session }),
74
85
  };
75
- if (readiness.brokerId !== undefined) {
76
- Object.assign(status, { brokerId: readiness.brokerId });
77
- }
78
- if (readiness.dpopThumbprint !== undefined) {
79
- Object.assign(status, { dpopThumbprint: readiness.dpopThumbprint });
80
- }
81
- if (remoteReadiness.session !== undefined) {
82
- Object.assign(status, {
83
- remoteSession: remoteReadiness.session,
84
- });
85
- }
86
86
  return status;
87
87
  }
88
+ export async function resolveBrokerStatusForDisplay(runtimePaths, secretStore, liveStatus) {
89
+ const brokerRunning = liveStatus !== null;
90
+ if (hasActionableRemoteStatus(liveStatus)) {
91
+ return liveStatus;
92
+ }
93
+ return await buildBrokerStatus(runtimePaths, secretStore, brokerRunning);
94
+ }
88
95
  export async function clearBrokerInstallation(runtimePaths, secretStore) {
89
96
  const metadata = await readBrokerMetadata(runtimePaths);
90
97
  if (metadata !== null) {
@@ -140,3 +147,8 @@ async function removeEmptyDirectory(directoryPath) {
140
147
  }
141
148
  }
142
149
  }
150
+ function hasActionableRemoteStatus(status) {
151
+ return (status !== null &&
152
+ typeof status.remoteAccessDetail === "string" &&
153
+ typeof status.remoteAccessState === "string");
154
+ }
@@ -2,10 +2,12 @@ import { fetchAuthorizationServerMetadata, } from "../auth/discovery.js";
2
2
  import { createDpopProof } from "../auth/dpop.js";
3
3
  import { refreshAccessToken } from "../auth/oauth.js";
4
4
  import { assertBrokerRemoteUrl } from "../auth/url-security.js";
5
- import { buildReauthenticationRequiredMessage, errorMessageIncludesReauthenticationCommand } from "../lib/user-guidance.js";
5
+ import { retryOperation } from "../lib/retry.js";
6
+ import { buildReauthenticationRequiredMessage, DRIGGSBY_LOGIN_COMMAND, DRIGGSBY_STATUS_COMMAND, errorMessageIncludesReauthenticationCommand, } from "../lib/user-guidance.js";
6
7
  import { readBrokerDpopKeyPair } from "./installation.js";
7
8
  import { readBrokerRemoteSession, summarizeBrokerRemoteSession, writeBrokerRemoteSession, } from "./session.js";
8
9
  const ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
10
+ const STATUS_REFRESH_RETRY_DELAYS_MS = [0, 250, 500];
9
11
  export class BrokerRemoteSessionManager {
10
12
  brokerId;
11
13
  fetchImpl;
@@ -39,16 +41,32 @@ export async function ensureFreshRemoteSession(options) {
39
41
  export async function inspectRemoteSessionReadiness(options) {
40
42
  const session = await readBrokerRemoteSession(options.secretStore, options.brokerId);
41
43
  if (session === null) {
44
+ return buildNotConnectedReadiness();
45
+ }
46
+ if (options.refreshIfNeeded && sessionNeedsRefresh(session)) {
47
+ try {
48
+ const refreshedSession = await retryOperation(async () => await ensureFreshRemoteSession(options), {
49
+ delaysMs: STATUS_REFRESH_RETRY_DELAYS_MS,
50
+ shouldRetry: shouldRetryStatusRefresh,
51
+ });
52
+ return buildReadyReadiness(refreshedSession);
53
+ }
54
+ catch (error) {
55
+ return buildFailedRefreshReadiness(error, session);
56
+ }
57
+ }
58
+ if (sessionNeedsRefresh(session)) {
42
59
  return {
43
- connected: false,
60
+ connected: true,
61
+ detail: "The saved remote session needs a token refresh before remote MCP forwarding can run.",
62
+ nextStepCommand: DRIGGSBY_STATUS_COMMAND,
44
63
  ready: false,
64
+ reauthenticationRequired: false,
65
+ session: summarizeBrokerRemoteSession(session),
66
+ state: "temporarily_unavailable",
45
67
  };
46
68
  }
47
- return {
48
- connected: true,
49
- ready: !sessionNeedsRefresh(session),
50
- session: summarizeBrokerRemoteSession(session),
51
- };
69
+ return buildReadyReadiness(session);
52
70
  }
53
71
  export function sessionNeedsRefresh(session, nowMs = Date.now()) {
54
72
  const expiresAtMs = Date.parse(session.accessTokenExpiresAt);
@@ -108,3 +126,48 @@ function throwRefreshSessionError(error) {
108
126
  }
109
127
  throw new Error("Driggsby could not refresh the local broker session right now. Wait a moment and try again.");
110
128
  }
129
+ function shouldRetryStatusRefresh(error) {
130
+ return !errorMessageIncludesReauthenticationCommand(error);
131
+ }
132
+ function buildFailedRefreshReadiness(error, session) {
133
+ if (errorMessageIncludesReauthenticationCommand(error)) {
134
+ return {
135
+ connected: true,
136
+ detail: "The saved remote session is no longer authorized. Remote MCP forwarding will stay blocked until Driggsby signs in again.",
137
+ nextStepCommand: DRIGGSBY_LOGIN_COMMAND,
138
+ ready: false,
139
+ reauthenticationRequired: true,
140
+ session: summarizeBrokerRemoteSession(session),
141
+ state: "reauth_required",
142
+ };
143
+ }
144
+ return {
145
+ connected: true,
146
+ detail: "Driggsby could not refresh remote MCP access right now. Remote MCP forwarding is blocked until the refresh succeeds.",
147
+ nextStepCommand: DRIGGSBY_STATUS_COMMAND,
148
+ ready: false,
149
+ reauthenticationRequired: false,
150
+ session: summarizeBrokerRemoteSession(session),
151
+ state: "temporarily_unavailable",
152
+ };
153
+ }
154
+ export function buildNotConnectedReadiness() {
155
+ return {
156
+ connected: false,
157
+ detail: "Driggsby does not have a saved remote session yet.",
158
+ nextStepCommand: DRIGGSBY_LOGIN_COMMAND,
159
+ ready: false,
160
+ reauthenticationRequired: false,
161
+ state: "not_connected",
162
+ };
163
+ }
164
+ function buildReadyReadiness(session) {
165
+ return {
166
+ connected: true,
167
+ detail: "Remote MCP forwarding is ready to use.",
168
+ ready: true,
169
+ reauthenticationRequired: false,
170
+ session: summarizeBrokerRemoteSession(session),
171
+ state: "ready",
172
+ };
173
+ }
@@ -1,5 +1,5 @@
1
1
  import { getBrokerStatus } from "../../broker/client.js";
2
- import { buildBrokerStatus } from "../../broker/installation.js";
2
+ import { resolveBrokerStatusForDisplay } from "../../broker/installation.js";
3
3
  import { KeyringSecretStore } from "../../broker/secret-store.js";
4
4
  import { formatStatusText } from "../format.js";
5
5
  export async function runStatusCommand(runtimePaths) {
@@ -8,7 +8,6 @@ export async function runStatusCommand(runtimePaths) {
8
8
  runtimePaths,
9
9
  secretStore,
10
10
  });
11
- const status = liveStatus ??
12
- (await buildBrokerStatus(runtimePaths, secretStore, false));
11
+ const status = await resolveBrokerStatusForDisplay(runtimePaths, secretStore, liveStatus);
13
12
  process.stdout.write(formatStatusText(status));
14
13
  }
@@ -1,23 +1,84 @@
1
+ import { DRIGGSBY_LOGIN_COMMAND, DRIGGSBY_STATUS_COMMAND, } from "../lib/user-guidance.js";
1
2
  export function formatStatusText(status) {
3
+ const remoteAccessState = resolveRemoteAccessState(status);
4
+ const remoteAccessDetail = resolveRemoteAccessDetail(status, remoteAccessState);
5
+ const nextStepCommand = resolveNextStepCommand(status, remoteAccessState);
2
6
  const lines = [
3
7
  "Local Driggsby broker",
4
- `installed: ${status.installed ? "yes" : "no"}`,
5
- `running: ${status.brokerRunning ? "yes" : "no"}`,
6
- `connected: ${status.remoteSession ? "yes" : "no"}`,
7
- `remote mcp ready: ${status.remoteMcpReady ? "yes" : "no"}`,
8
- `socket: ${status.socketPath}`,
8
+ `remote MCP forwarding: ${status.remoteMcpReady ? "ready" : "not ready"}`,
9
+ `remote access state: ${formatRemoteAccessState(remoteAccessState)}`,
10
+ `what this means: ${remoteAccessDetail}`,
9
11
  ];
10
- if (status.brokerId) {
11
- lines.push(`broker id: ${status.brokerId}`);
12
+ if (!status.installed) {
13
+ lines.push("local broker setup: incomplete");
12
14
  }
13
- if (status.dpopThumbprint) {
14
- lines.push(`dpop thumbprint: ${status.dpopThumbprint}`);
15
+ if (!status.brokerRunning) {
16
+ lines.push("local broker service: not running");
17
+ }
18
+ if (nextStepCommand) {
19
+ lines.push(`next step: run \`${nextStepCommand}\``);
15
20
  }
16
21
  if (status.remoteSession) {
17
- lines.push(`server: ${status.remoteSession.issuer}`);
18
- lines.push(`resource: ${status.remoteSession.resource}`);
19
- lines.push(`scope: ${status.remoteSession.scope}`);
20
- lines.push(`access token expires: ${status.remoteSession.accessTokenExpiresAt}`);
22
+ lines.push(`saved access token expires at: ${status.remoteSession.accessTokenExpiresAt}`);
21
23
  }
22
24
  return `${lines.join("\n")}\n`;
23
25
  }
26
+ function formatRemoteAccessState(remoteAccessState) {
27
+ switch (remoteAccessState) {
28
+ case "ready":
29
+ return "authenticated";
30
+ case "not_connected":
31
+ return "not connected";
32
+ case "reauth_required":
33
+ return "re-authentication required";
34
+ case "temporarily_unavailable":
35
+ return "temporarily unavailable";
36
+ }
37
+ return remoteAccessState;
38
+ }
39
+ function resolveRemoteAccessState(status) {
40
+ const runtimeStatus = status;
41
+ return runtimeStatus.remoteAccessState ?? inferLegacyRemoteAccessState(status);
42
+ }
43
+ function resolveRemoteAccessDetail(status, remoteAccessState) {
44
+ const runtimeStatus = status;
45
+ return runtimeStatus.remoteAccessDetail ??
46
+ inferLegacyRemoteAccessDetail(status, remoteAccessState);
47
+ }
48
+ function resolveNextStepCommand(status, remoteAccessState) {
49
+ const runtimeStatus = status;
50
+ return runtimeStatus.nextStepCommand ??
51
+ inferLegacyNextStepCommand(remoteAccessState);
52
+ }
53
+ function inferLegacyRemoteAccessState(status) {
54
+ if (status.remoteMcpReady) {
55
+ return "ready";
56
+ }
57
+ if (status.remoteSession) {
58
+ return "temporarily_unavailable";
59
+ }
60
+ return "not_connected";
61
+ }
62
+ function inferLegacyRemoteAccessDetail(status, remoteAccessState) {
63
+ if (remoteAccessState === "ready") {
64
+ return "Remote MCP forwarding is ready to use.";
65
+ }
66
+ if (remoteAccessState === "not_connected") {
67
+ return "Driggsby does not have a saved remote session yet.";
68
+ }
69
+ if (status.remoteSession) {
70
+ return "Remote MCP forwarding is not ready yet. Driggsby may still need to refresh the saved remote session before forwarding can run.";
71
+ }
72
+ return "Remote MCP forwarding is not ready to use yet.";
73
+ }
74
+ function inferLegacyNextStepCommand(remoteAccessState) {
75
+ switch (remoteAccessState) {
76
+ case "ready":
77
+ return undefined;
78
+ case "not_connected":
79
+ case "reauth_required":
80
+ return DRIGGSBY_LOGIN_COMMAND;
81
+ case "temporarily_unavailable":
82
+ return DRIGGSBY_STATUS_COMMAND;
83
+ }
84
+ }
@@ -3,11 +3,11 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { ensureBrokerRunning } from "../broker/launch.js";
5
5
  import { callBrokerTool, getBrokerStatus, listBrokerTools, } from "../broker/client.js";
6
- import { buildBrokerStatus } from "../broker/installation.js";
6
+ import { resolveBrokerStatusForDisplay } from "../broker/installation.js";
7
7
  import { KeyringSecretStore } from "../broker/secret-store.js";
8
8
  import { retryOperation } from "../lib/retry.js";
9
9
  import { buildBrokerInvestigationMessage, errorMessageIncludesReauthenticationCommand, formatRetryWindow, } from "../lib/user-guidance.js";
10
- import { formatStatusText } from "../cli/format.js";
10
+ import { formatStatusText, } from "../cli/format.js";
11
11
  import { LifecycleAwareStdioServerTransport } from "./stdio-transport.js";
12
12
  const LOCAL_STATUS_TOOL = {
13
13
  description: "Report readiness and connectivity for the shared local Driggsby broker.",
@@ -59,8 +59,8 @@ export function createLocalShimServer(runtimePaths, secretStore) {
59
59
  return server;
60
60
  }
61
61
  async function buildLocalStatusToolResult(runtimePaths, secretStore) {
62
- const status = (await getBrokerStatus({ runtimePaths, secretStore })) ??
63
- (await buildBrokerStatus(runtimePaths, secretStore, false));
62
+ const liveStatus = await getBrokerStatus({ runtimePaths, secretStore });
63
+ const status = await resolveBrokerStatusForDisplay(runtimePaths, secretStore, liveStatus);
64
64
  return {
65
65
  content: [
66
66
  {
@@ -3,20 +3,29 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  export class LifecycleAwareStdioServerTransport {
4
4
  delegate;
5
5
  stdin;
6
+ parentPollIntervalMs;
7
+ parentProcess;
8
+ startingParentPid;
6
9
  closingPromise = null;
10
+ parentPollHandle = null;
7
11
  started = false;
8
12
  onclose;
9
13
  onerror;
10
14
  onmessage;
11
15
  handleInputClosed = () => {
12
- void this.close().catch((error) => {
13
- this.onerror?.(error instanceof Error
14
- ? error
15
- : new Error("The Driggsby MCP transport could not close cleanly."));
16
- });
16
+ this.closeForLifecycleEnd("input stream closed");
17
17
  };
18
- constructor(stdin = process.stdin, stdout = process.stdout) {
18
+ handleParentProcessChanged = () => {
19
+ if (this.parentProcess.ppid === this.startingParentPid) {
20
+ return;
21
+ }
22
+ this.closeForLifecycleEnd("parent process exited");
23
+ };
24
+ constructor(stdin = process.stdin, stdout = process.stdout, options = {}) {
19
25
  this.stdin = stdin;
26
+ this.parentPollIntervalMs = options.parentPollIntervalMs ?? 1_000;
27
+ this.parentProcess = options.parentProcess ?? process;
28
+ this.startingParentPid = this.parentProcess.ppid;
20
29
  this.delegate = new StdioServerTransport(stdin, stdout);
21
30
  this.delegate.onclose = () => {
22
31
  this.onclose?.();
@@ -35,11 +44,13 @@ export class LifecycleAwareStdioServerTransport {
35
44
  this.started = true;
36
45
  this.stdin.once("close", this.handleInputClosed);
37
46
  this.stdin.once("end", this.handleInputClosed);
47
+ this.startParentWatchdog();
38
48
  try {
39
49
  await this.delegate.start();
40
50
  }
41
51
  catch (error) {
42
52
  this.removeInputListeners();
53
+ this.stopParentWatchdog();
43
54
  this.started = false;
44
55
  throw error;
45
56
  }
@@ -55,6 +66,7 @@ export class LifecycleAwareStdioServerTransport {
55
66
  }
56
67
  this.started = false;
57
68
  this.removeInputListeners();
69
+ this.stopParentWatchdog();
58
70
  await this.delegate.close();
59
71
  })();
60
72
  try {
@@ -67,8 +79,28 @@ export class LifecycleAwareStdioServerTransport {
67
79
  async send(message) {
68
80
  await this.delegate.send(message);
69
81
  }
82
+ closeForLifecycleEnd(reason) {
83
+ void this.close().catch((error) => {
84
+ this.onerror?.(error instanceof Error
85
+ ? error
86
+ : new Error(`The Driggsby MCP transport could not close cleanly after ${reason}.`));
87
+ });
88
+ }
70
89
  removeInputListeners() {
71
90
  this.stdin.off("close", this.handleInputClosed);
72
91
  this.stdin.off("end", this.handleInputClosed);
73
92
  }
93
+ startParentWatchdog() {
94
+ this.parentPollHandle = setInterval(() => {
95
+ this.handleParentProcessChanged();
96
+ }, this.parentPollIntervalMs);
97
+ this.parentPollHandle.unref();
98
+ }
99
+ stopParentWatchdog() {
100
+ if (this.parentPollHandle === null) {
101
+ return;
102
+ }
103
+ clearInterval(this.parentPollHandle);
104
+ this.parentPollHandle = null;
105
+ }
74
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Local MCP broker and CLI for connecting AI clients to Driggsby",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",