driggsby 0.1.4 → 0.1.6

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 CHANGED
@@ -48,11 +48,6 @@ driggsby logout
48
48
  driggsby broker-daemon
49
49
  ```
50
50
 
51
- ## Learn More
52
-
53
- - Product and source: https://github.com/thegoodsoftwareco/driggsby
54
- - Issues: https://github.com/thegoodsoftwareco/driggsby/issues
55
-
56
51
  ## License
57
52
 
58
53
  This package is currently published as `UNLICENSED`.
@@ -6,6 +6,7 @@ import { fetchAuthorizationServerMetadata, fetchProtectedResourceMetadata, } fro
6
6
  import { createDpopProof } from "./dpop.js";
7
7
  import { startLoopbackAuthListener } from "./loopback.js";
8
8
  import { buildAuthorizationUrl, createOAuthState, exchangeAuthorizationCode, registerBrokerClient, } from "./oauth.js";
9
+ import { buildReauthenticationRequiredMessage } from "../lib/user-guidance.js";
9
10
  import { generatePkcePair } from "./pkce.js";
10
11
  import { assertBrokerRemoteUrl } from "./url-security.js";
11
12
  export async function loginBroker(runtimePaths, dependencies) {
@@ -114,7 +115,7 @@ function validateRemoteMetadata(authorizationServerMetadata, protectedResourceMe
114
115
  async function tokenEndpointDpopProof(runtimePaths, secretStore, brokerId, tokenEndpoint) {
115
116
  const dpopKeyPair = await readBrokerDpopKeyPair(runtimePaths, secretStore, brokerId);
116
117
  if (dpopKeyPair === null) {
117
- throw new Error("The local broker DPoP key is missing. Run `npx -y driggsby login` again.");
118
+ throw new Error(buildReauthenticationRequiredMessage("The local broker DPoP key is missing"));
118
119
  }
119
120
  return await createDpopProof({
120
121
  httpMethod: "POST",
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { z } from "zod";
3
+ import { buildReauthenticationRequiredMessage } from "../lib/user-guidance.js";
3
4
  const registrationResponseSchema = z.object({
4
5
  client_id: z.string().min(1),
5
6
  scope: z.string().min(1),
@@ -87,7 +88,11 @@ export async function refreshAccessToken(options, fetchImpl = fetch) {
87
88
  method: "POST",
88
89
  });
89
90
  if (!response.ok) {
90
- throw new Error("Driggsby could not refresh the broker session with the remote service.");
91
+ const errorBody = await readErrorBody(response);
92
+ if (isAuthenticationFailure(response.status, errorBody)) {
93
+ throw new Error(buildReauthenticationRequiredMessage("Authentication has expired or the saved broker session is no longer valid"));
94
+ }
95
+ throw new Error("Driggsby could not refresh the broker session with the remote service. Wait a moment and try again.");
91
96
  }
92
97
  const refreshedTokens = parseTokenExchangeResult(await response.json(), false);
93
98
  return {
@@ -95,6 +100,24 @@ export async function refreshAccessToken(options, fetchImpl = fetch) {
95
100
  refreshToken: refreshedTokens.refreshToken || options.refreshToken,
96
101
  };
97
102
  }
103
+ async function readErrorBody(response) {
104
+ try {
105
+ return (await response.text()).toLowerCase();
106
+ }
107
+ catch {
108
+ return "";
109
+ }
110
+ }
111
+ function isAuthenticationFailure(status, errorBody) {
112
+ return (status === 401 ||
113
+ status === 403 ||
114
+ (status === 400 &&
115
+ (errorBody.includes("invalid_grant") ||
116
+ errorBody.includes("invalid_token") ||
117
+ errorBody.includes("expired") ||
118
+ errorBody.includes("revoked") ||
119
+ errorBody.includes("unauthorized_client"))));
120
+ }
98
121
  export function createOAuthState() {
99
122
  return randomUUID();
100
123
  }
@@ -3,6 +3,7 @@ import { BrokerRemoteSessionManager } from "./remote-session.js";
3
3
  import { callRemoteTool, listRemoteTools } from "./remote-mcp.js";
4
4
  import { KeyringSecretStore } from "./secret-store.js";
5
5
  import { LocalBrokerServer } from "./server.js";
6
+ import { buildReauthenticationRequiredMessage } from "../lib/user-guidance.js";
6
7
  export async function runBrokerDaemon(runtimePaths) {
7
8
  const secretStore = new KeyringSecretStore();
8
9
  const metadata = await ensureBrokerInstallation(runtimePaths, secretStore);
@@ -44,21 +45,21 @@ export async function runBrokerDaemon(runtimePaths) {
44
45
  async function readRequiredLocalAuthToken(secretStore, brokerId) {
45
46
  const localAuthToken = await readBrokerLocalAuthToken(secretStore, brokerId);
46
47
  if (localAuthToken === null) {
47
- throw new Error("The local broker auth state is incomplete. Run `npx -y driggsby login` again.");
48
+ throw new Error(buildReauthenticationRequiredMessage("The local broker auth state is incomplete"));
48
49
  }
49
50
  return localAuthToken;
50
51
  }
51
52
  async function readRequiredPrivateJwk(secretStore, brokerId) {
52
53
  const privateJwkRaw = await readBrokerPrivateJwk(secretStore, brokerId);
53
54
  if (privateJwkRaw === null) {
54
- throw new Error("The local broker signing key is missing. Run `npx -y driggsby login` again.");
55
+ throw new Error(buildReauthenticationRequiredMessage("The local broker signing key is missing"));
55
56
  }
56
57
  return JSON.parse(privateJwkRaw);
57
58
  }
58
59
  async function readRequiredDpopKeyPair(runtimePaths, secretStore, brokerId) {
59
60
  const dpopKeyPair = await readBrokerDpopKeyPair(runtimePaths, secretStore, brokerId);
60
61
  if (dpopKeyPair === null) {
61
- throw new Error("The local broker DPoP key is missing. Run `npx -y driggsby login` again.");
62
+ throw new Error(buildReauthenticationRequiredMessage("The local broker DPoP key is missing"));
62
63
  }
63
64
  return dpopKeyPair;
64
65
  }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { setTimeout as sleep } from "node:timers/promises";
3
+ import { buildBrokerInvestigationMessage } from "../lib/user-guidance.js";
3
4
  import { getBrokerStatus, pingBroker } from "./client.js";
4
5
  export async function ensureBrokerRunning(options) {
5
6
  if ((await pingBroker({
@@ -11,7 +12,7 @@ export async function ensureBrokerRunning(options) {
11
12
  spawnBrokerDaemon(options.entrypointPath);
12
13
  const started = await waitForBroker(options.runtimePaths, options.secretStore, 4_000);
13
14
  if (!started) {
14
- throw new Error("The local Driggsby broker did not start cleanly. Try `npx -y driggsby login` again.");
15
+ throw new Error(buildBrokerInvestigationMessage("The local Driggsby broker did not start cleanly"));
15
16
  }
16
17
  }
17
18
  export async function waitForBroker(runtimePaths, secretStore, timeoutMs) {
@@ -3,6 +3,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
3
3
  import { CallToolResultSchema, ListToolsResultSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { createDpopProof } from "../auth/dpop.js";
5
5
  import { assertBrokerRemoteUrl } from "../auth/url-security.js";
6
+ import { buildReauthenticationRequiredMessage } from "../lib/user-guidance.js";
6
7
  const BROKER_CLIENT_INFO = {
7
8
  name: "driggsby-local-broker",
8
9
  version: "0.1.0",
@@ -94,13 +95,20 @@ function createDpopAuthenticatedFetch(session, dpopKeyPair) {
94
95
  publicJwk: dpopKeyPair.publicJwk,
95
96
  targetUrl: resolveRequestUrl(input),
96
97
  }));
97
- return await fetch(input, {
98
+ return await ensureAuthenticatedRemoteResponse(fetch(input, {
98
99
  ...init,
99
100
  headers,
100
101
  method: httpMethod,
101
- });
102
+ }));
102
103
  };
103
104
  }
105
+ async function ensureAuthenticatedRemoteResponse(responsePromise) {
106
+ const response = await responsePromise;
107
+ if (response.status === 401 || response.status === 403) {
108
+ throw new Error(buildReauthenticationRequiredMessage("Authentication has expired or the saved broker session is no longer authorized for Driggsby"));
109
+ }
110
+ return response;
111
+ }
104
112
  function resolveRequestMethod(input, init) {
105
113
  if (init?.method !== undefined) {
106
114
  return init.method;
@@ -2,6 +2,7 @@ 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
6
  import { readBrokerDpopKeyPair } from "./installation.js";
6
7
  import { readBrokerRemoteSession, summarizeBrokerRemoteSession, writeBrokerRemoteSession, } from "./session.js";
7
8
  const ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
@@ -20,7 +21,7 @@ export class BrokerRemoteSessionManager {
20
21
  async ensureFreshSession() {
21
22
  const session = await readBrokerRemoteSession(this.secretStore, this.brokerId);
22
23
  if (session === null) {
23
- throw new Error("The local Driggsby broker is not connected. Run `npx -y driggsby login` again.");
24
+ throw new Error(buildReauthenticationRequiredMessage("The local Driggsby broker is not connected"));
24
25
  }
25
26
  if (!sessionNeedsRefresh(session)) {
26
27
  return session;
@@ -63,7 +64,7 @@ async function refreshRemoteSession(runtimePaths, secretStore, brokerId, session
63
64
  authorizationServerMetadata = await fetchAuthorizationServerMetadata(session.issuer, fetchImpl);
64
65
  }
65
66
  catch {
66
- throw new Error("Driggsby could not refresh the local broker session right now. Try again in a moment.");
67
+ throw new Error("Driggsby could not refresh the local broker session right now because it could not reach the authorization server. Wait a moment and try again.");
67
68
  }
68
69
  let refreshedTokens;
69
70
  try {
@@ -75,8 +76,8 @@ async function refreshRemoteSession(runtimePaths, secretStore, brokerId, session
75
76
  resource: session.resource,
76
77
  }, fetchImpl);
77
78
  }
78
- catch {
79
- throw new Error("The local Driggsby broker session expired. Run `npx -y driggsby login` again.");
79
+ catch (error) {
80
+ throwRefreshSessionError(error);
80
81
  }
81
82
  const refreshedSession = {
82
83
  ...session,
@@ -92,7 +93,7 @@ async function refreshRemoteSession(runtimePaths, secretStore, brokerId, session
92
93
  async function refreshTokenDpopProof(runtimePaths, secretStore, brokerId, tokenEndpoint) {
93
94
  const dpopKeyPair = await readBrokerDpopKeyPair(runtimePaths, secretStore, brokerId);
94
95
  if (dpopKeyPair === null) {
95
- throw new Error("The local Driggsby broker key is missing. Run `npx -y driggsby login` again.");
96
+ throw new Error(buildReauthenticationRequiredMessage("The local Driggsby broker key is missing"));
96
97
  }
97
98
  return await createDpopProof({
98
99
  httpMethod: "POST",
@@ -101,3 +102,9 @@ async function refreshTokenDpopProof(runtimePaths, secretStore, brokerId, tokenE
101
102
  targetUrl: tokenEndpoint,
102
103
  });
103
104
  }
105
+ function throwRefreshSessionError(error) {
106
+ if (errorMessageIncludesReauthenticationCommand(error)) {
107
+ throw error;
108
+ }
109
+ throw new Error("Driggsby could not refresh the local broker session right now. Wait a moment and try again.");
110
+ }
@@ -0,0 +1,22 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ export async function retryOperation(operation, options) {
3
+ let lastError;
4
+ const totalAttempts = options.delaysMs.length;
5
+ for (const [index, delayMs] of options.delaysMs.entries()) {
6
+ if (delayMs > 0) {
7
+ await sleep(delayMs);
8
+ }
9
+ try {
10
+ return await operation(index + 1, totalAttempts);
11
+ }
12
+ catch (error) {
13
+ lastError = error;
14
+ if (index === totalAttempts - 1 || !options.shouldRetry(error)) {
15
+ throw error;
16
+ }
17
+ }
18
+ }
19
+ throw lastError instanceof Error
20
+ ? lastError
21
+ : new Error("The retry operation did not complete successfully.");
22
+ }
@@ -0,0 +1,19 @@
1
+ export const DRIGGSBY_LOGIN_COMMAND = "npx driggsby@latest login";
2
+ export const DRIGGSBY_STATUS_COMMAND = "npx driggsby@latest status";
3
+ export function buildReauthenticationRequiredMessage(reason) {
4
+ return `${ensureTrailingPeriod(reason)} Please re-authenticate by running \`${DRIGGSBY_LOGIN_COMMAND}\`.`;
5
+ }
6
+ export function buildBrokerInvestigationMessage(reason) {
7
+ return `${ensureTrailingPeriod(reason)} Investigate broker readiness by running \`${DRIGGSBY_STATUS_COMMAND}\`. If authentication expired, re-authenticate with \`${DRIGGSBY_LOGIN_COMMAND}\`.`;
8
+ }
9
+ export function errorMessageIncludesReauthenticationCommand(error) {
10
+ return (error instanceof Error &&
11
+ error.message.includes(DRIGGSBY_LOGIN_COMMAND));
12
+ }
13
+ export function formatRetryWindow(delaysMs) {
14
+ const totalDelayMs = delaysMs.reduce((sum, delay) => sum + delay, 0);
15
+ return `${(totalDelayMs / 1_000).toFixed(1)} seconds`;
16
+ }
17
+ function ensureTrailingPeriod(value) {
18
+ return /[.!?]$/.test(value) ? value : `${value}.`;
19
+ }
@@ -6,6 +6,8 @@ import { ensureBrokerRunning } from "../broker/launch.js";
6
6
  import { callBrokerTool, getBrokerStatus, listBrokerTools, } from "../broker/client.js";
7
7
  import { buildBrokerStatus } from "../broker/installation.js";
8
8
  import { KeyringSecretStore } from "../broker/secret-store.js";
9
+ import { retryOperation } from "../lib/retry.js";
10
+ import { buildBrokerInvestigationMessage, errorMessageIncludesReauthenticationCommand, formatRetryWindow, } from "../lib/user-guidance.js";
9
11
  import { formatStatusText } from "../cli/format.js";
10
12
  const LOCAL_STATUS_TOOL = {
11
13
  description: "Report readiness and connectivity for the shared local Driggsby broker.",
@@ -16,6 +18,7 @@ const LOCAL_STATUS_TOOL = {
16
18
  },
17
19
  name: "get_local_broker_status",
18
20
  };
21
+ const BROKER_OPERATION_RETRY_DELAYS_MS = [0, 250, 500, 1_000, 2_000];
19
22
  export async function runMcpServerCommand(runtimePaths, entrypointPath) {
20
23
  const secretStore = new KeyringSecretStore();
21
24
  await ensureBrokerRunning({
@@ -36,7 +39,7 @@ export function createLocalShimServer(runtimePaths, secretStore) {
36
39
  },
37
40
  });
38
41
  server.setRequestHandler(ListToolsRequestSchema, async () => {
39
- const remoteTools = await loadRemoteTools(runtimePaths, secretStore);
42
+ const remoteTools = await loadRemoteToolsOrThrow(runtimePaths, secretStore);
40
43
  return {
41
44
  tools: [LOCAL_STATUS_TOOL, ...remoteTools],
42
45
  };
@@ -45,23 +48,11 @@ export function createLocalShimServer(runtimePaths, secretStore) {
45
48
  if (request.params.name === LOCAL_STATUS_TOOL.name) {
46
49
  return await buildLocalStatusToolResult(runtimePaths, secretStore);
47
50
  }
48
- const remoteTools = await listBrokerTools({
49
- runtimePaths,
50
- secretStore,
51
- });
52
- if (remoteTools === null) {
53
- throw new Error("The local Driggsby broker could not load the remote Driggsby tools right now. Use `get_local_broker_status` for details, then try again.");
54
- }
51
+ const remoteTools = await loadRemoteToolsOrThrow(runtimePaths, secretStore);
55
52
  if (!remoteTools.some((tool) => tool.name === request.params.name)) {
56
53
  throw new Error("That Driggsby tool is not available in this session anymore. Ask the client to refresh its tool list and try again.");
57
54
  }
58
- const toolResult = await callBrokerTool({
59
- runtimePaths,
60
- secretStore,
61
- }, request.params.name, asToolArguments(request.params.arguments));
62
- if (toolResult === null) {
63
- throw new Error("The local Driggsby broker could not reach Driggsby right now. Use `get_local_broker_status` for details, then try again.");
64
- }
55
+ const toolResult = await callBrokerToolOrThrow(runtimePaths, secretStore, request.params.name, asToolArguments(request.params.arguments));
65
56
  return toolResult;
66
57
  });
67
58
  return server;
@@ -89,15 +80,63 @@ function asToolArguments(argumentsValue) {
89
80
  }
90
81
  return argumentsValue;
91
82
  }
92
- async function loadRemoteTools(runtimePaths, secretStore) {
83
+ async function loadRemoteToolsOrThrow(runtimePaths, secretStore) {
84
+ try {
85
+ return await retryOperation(async () => {
86
+ const remoteTools = await listBrokerTools({
87
+ runtimePaths,
88
+ secretStore,
89
+ });
90
+ if (remoteTools === null) {
91
+ throw new Error("The local Driggsby broker is not responding yet.");
92
+ }
93
+ return remoteTools;
94
+ }, {
95
+ delaysMs: BROKER_OPERATION_RETRY_DELAYS_MS,
96
+ shouldRetry: shouldRetryBrokerOperation,
97
+ });
98
+ }
99
+ catch (error) {
100
+ if (errorMessageIncludesReauthenticationCommand(error)) {
101
+ throw error;
102
+ }
103
+ throw new Error(buildBrokerInvestigationMessage(`The local Driggsby broker could not load the Driggsby tool list after ${BROKER_OPERATION_RETRY_DELAYS_MS.length} attempts over ${formatRetryWindow(BROKER_OPERATION_RETRY_DELAYS_MS)}`));
104
+ }
105
+ }
106
+ async function callBrokerToolOrThrow(runtimePaths, secretStore, toolName, args) {
93
107
  try {
94
- const remoteTools = await listBrokerTools({
95
- runtimePaths,
96
- secretStore,
108
+ return await retryOperation(async () => {
109
+ const toolResult = await callBrokerTool({
110
+ runtimePaths,
111
+ secretStore,
112
+ }, toolName, args);
113
+ if (toolResult === null) {
114
+ throw new Error("The local Driggsby broker is not responding yet.");
115
+ }
116
+ return toolResult;
117
+ }, {
118
+ delaysMs: BROKER_OPERATION_RETRY_DELAYS_MS,
119
+ shouldRetry: shouldRetryBrokerOperation,
97
120
  });
98
- return remoteTools ?? [];
99
121
  }
100
- catch {
101
- return [];
122
+ catch (error) {
123
+ if (errorMessageIncludesReauthenticationCommand(error)) {
124
+ throw error;
125
+ }
126
+ if (error instanceof Error && error.message.includes("not available in this session anymore")) {
127
+ throw error;
128
+ }
129
+ throw new Error(buildBrokerInvestigationMessage(`The local Driggsby broker could not run \`${toolName}\` after ${BROKER_OPERATION_RETRY_DELAYS_MS.length} attempts over ${formatRetryWindow(BROKER_OPERATION_RETRY_DELAYS_MS)}`));
130
+ }
131
+ }
132
+ function shouldRetryBrokerOperation(error) {
133
+ if (errorMessageIncludesReauthenticationCommand(error)) {
134
+ return false;
135
+ }
136
+ if (!(error instanceof Error)) {
137
+ return true;
102
138
  }
139
+ const message = error.message.toLowerCase();
140
+ return (!message.includes("not available in this session anymore") &&
141
+ !message.includes("invalid tool arguments"));
103
142
  }
package/package.json CHANGED
@@ -1,18 +1,9 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Local MCP broker and CLI for connecting AI clients to Driggsby",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/thegoodsoftwareco/driggsby.git",
10
- "directory": "packages/driggsby"
11
- },
12
- "homepage": "https://github.com/thegoodsoftwareco/driggsby/tree/main/packages/driggsby#readme",
13
- "bugs": {
14
- "url": "https://github.com/thegoodsoftwareco/driggsby/issues"
15
- },
16
7
  "keywords": [
17
8
  "driggsby",
18
9
  "mcp",