grain-tools 0.10.12 → 0.10.13

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/dist/mcp-app.html CHANGED
@@ -148961,7 +148961,7 @@ function App() {
148961
148961
  var _a3, _b3;
148962
148962
  if (!isReady || !activeTab) return;
148963
148963
  const tabId = activeTab.id;
148964
- updateTab(tabId, { isLoading: true, error: null });
148964
+ updateTab(tabId, { isLoading: true, error: null, cancelled: false, errorDetails: null });
148965
148965
  try {
148966
148966
  const callResult = await callServerTool("run_query", { sql, session: "mcp-app-manual" });
148967
148967
  if (callResult.isError) {
@@ -149105,7 +149105,10 @@ function App() {
149105
149105
  data: result,
149106
149106
  domLayout: "autoHeight"
149107
149107
  }
149108
- ) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "sp-empty", children: "No results yet." }) })
149108
+ ) }) : activeTab.cancelled ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "sp-cancelled-bar sp-empty", children: [
149109
+ "Query cancelled",
149110
+ activeTab.errorDetails ? `: ${activeTab.errorDetails}` : ""
149111
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "sp-empty", children: "No results yet." }) })
149109
149112
  ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ConnectionsManager, { theme: theme2 })
149110
149113
  ] })
149111
149114
  ] });
@@ -19277,13 +19277,27 @@ var init_StdioBridge = __esm({
19277
19277
  }
19278
19278
  }
19279
19279
  async forwardAgentMessage(message, fatal) {
19280
+ const isInitialize = "method" in message && message.method === "initialize";
19281
+ const isHandshake = isInitialize || "method" in message && message.method === "notifications/initialized";
19282
+ if (!isInitialize && this.sessionOpening) {
19283
+ await this.sessionOpening;
19284
+ }
19280
19285
  if (this.reconnecting) {
19281
- this.queuedAgentMessages.push(message);
19286
+ if (!isHandshake) {
19287
+ this.queuedAgentMessages.push(message);
19288
+ }
19282
19289
  return;
19283
19290
  }
19291
+ const sending = this.daemonTransport.send(message);
19292
+ if (isInitialize) {
19293
+ this.sessionOpening = sending.catch(() => void 0);
19294
+ }
19284
19295
  try {
19285
- await this.daemonTransport.send(message);
19296
+ await sending;
19286
19297
  } catch {
19298
+ if (!isHandshake) {
19299
+ this.queuedAgentMessages.push(message);
19300
+ }
19287
19301
  void this.reconnect(fatal);
19288
19302
  }
19289
19303
  }
@@ -20473,7 +20487,11 @@ var init_TelemetrySchema = __esm({
20473
20487
  duration_bucket: "durationBucket",
20474
20488
  error_kind: "errorKind"
20475
20489
  },
20476
- ledger_health: { entries_bucket: "rowCountBucket", write_failures_bucket: "rowCountBucket" },
20490
+ ledger_health: {
20491
+ entries_bucket: "rowCountBucket",
20492
+ write_failures_bucket: "rowCountBucket",
20493
+ outcome: "safeString"
20494
+ },
20477
20495
  ledger_verify_completed: {
20478
20496
  outcome: "safeString",
20479
20497
  entries_checked_bucket: "rowCountBucket",
@@ -21816,7 +21834,7 @@ var require_package = __commonJS({
21816
21834
  name: "grain",
21817
21835
  displayName: "Grain SQL",
21818
21836
  description: "Run SQL in VS Code and expose safe, visible MCP database tools for Claude, Cursor, and other AI agents.",
21819
- version: "0.10.12",
21837
+ version: "0.10.13",
21820
21838
  license: "UNLICENSED",
21821
21839
  mcpName: "io.github.pattrnlabs/grain",
21822
21840
  publisher: "pattrnlabs",
@@ -101556,13 +101574,26 @@ var init_QueryOperationRunner = __esm({
101556
101574
  sql
101557
101575
  };
101558
101576
  }
101577
+ getNextResultNumber(session) {
101578
+ let maxCount = 0;
101579
+ session.tabs.forEach((tab) => {
101580
+ const match = tab.title.match(/^Result (\d+)$/);
101581
+ if (match && match[1]) {
101582
+ const count2 = parseInt(match[1], 10);
101583
+ if (count2 > maxCount) {
101584
+ maxCount = count2;
101585
+ }
101586
+ }
101587
+ });
101588
+ return maxCount + 1;
101589
+ }
101559
101590
  resolveTab(session, providedTabId, shouldCreateNewTab, providedTitle) {
101560
101591
  if (providedTabId) {
101561
101592
  const tab = session.tabs.get(providedTabId);
101562
101593
  return {
101563
101594
  tab,
101564
101595
  tabId: providedTabId,
101565
- tabTitle: providedTitle || tab?.title || `Result ${session.tabs.size + 1}`
101596
+ tabTitle: providedTitle || tab?.title || `Result ${this.getNextResultNumber(session)}`
101566
101597
  };
101567
101598
  }
101568
101599
  if (!shouldCreateNewTab && session.activeTabId) {
@@ -101578,7 +101609,7 @@ var init_QueryOperationRunner = __esm({
101578
101609
  return {
101579
101610
  tab: void 0,
101580
101611
  tabId: `t${Math.random().toString(36).substring(2, 10)}`,
101581
- tabTitle: providedTitle || `Result ${session.tabs.size + 1}`
101612
+ tabTitle: providedTitle || `Result ${this.getNextResultNumber(session)}`
101582
101613
  };
101583
101614
  }
101584
101615
  async executeAndStore(sessionId, tabId, sql, connectionId, connectionProfile, authHeader, controller, origin2, loadAllRows, enforceReadOnly, clientConsent, workspaceRoot) {
@@ -183984,13 +184015,25 @@ This link expires at ${new Date(
183984
184015
  };
183985
184016
  }
183986
184017
  async handleLedgerStats(clientConsent) {
183987
- if (!this.ledger)
184018
+ if (!this.ledger) {
184019
+ this.options.telemetry?.capture(
184020
+ "ledger_health",
184021
+ {
184022
+ state: "daemon",
184023
+ outcome: "disabled",
184024
+ entries_bucket: "0",
184025
+ write_failures_bucket: "0"
184026
+ },
184027
+ clientConsent
184028
+ );
183988
184029
  return textToolResult("Ledger disabled.");
184030
+ }
183989
184031
  const stats = await this.ledger.stats();
183990
184032
  this.options.telemetry?.capture(
183991
184033
  "ledger_health",
183992
184034
  {
183993
184035
  state: "daemon",
184036
+ outcome: "enabled",
183994
184037
  entries_bucket: bucketRowCount(stats.persisted_entries),
183995
184038
  write_failures_bucket: bucketRowCount(stats.write_failures)
183996
184039
  },
@@ -183999,8 +184042,19 @@ This link expires at ${new Date(
183999
184042
  return { content: [{ type: "text", text: JSON.stringify(stats) }], structuredContent: stats };
184000
184043
  }
184001
184044
  async handleVerifyLedger(clientConsent) {
184002
- if (!this.ledger)
184045
+ if (!this.ledger) {
184046
+ this.options.telemetry?.capture(
184047
+ "ledger_verify_completed",
184048
+ {
184049
+ state: "daemon",
184050
+ outcome: "disabled",
184051
+ entries_checked_bucket: "0",
184052
+ duration_bucket: "<100ms"
184053
+ },
184054
+ clientConsent
184055
+ );
184003
184056
  return textToolResult("Ledger disabled.");
184057
+ }
184004
184058
  const startedAt = Date.now();
184005
184059
  const result = await verifyLedger(this.ledger);
184006
184060
  this.options.telemetry?.capture(
@@ -251308,7 +251362,14 @@ function confirmsExpectedInstance(expected, identity2) {
251308
251362
  if (expected.token && identity2.pid === void 0) {
251309
251363
  return false;
251310
251364
  }
251311
- return classifyRecordInstance(expected, identity2, Date.now()) !== "mismatch";
251365
+ if (expected.pid === void 0 || expected.startedAt === void 0) {
251366
+ return true;
251367
+ }
251368
+ return classifyRecordInstance(
251369
+ { pid: expected.pid, startedAt: expected.startedAt },
251370
+ identity2,
251371
+ Date.now()
251372
+ ) !== "mismatch";
251312
251373
  }
251313
251374
  function probeSharedDaemonOnce(port, timeoutMs, expected) {
251314
251375
  return new Promise((resolve11) => {
@@ -251363,10 +251424,33 @@ function probeSharedDaemonOnce(port, timeoutMs, expected) {
251363
251424
  });
251364
251425
  });
251365
251426
  }
251427
+ function isTokenAccepted(port, token, timeoutMs) {
251428
+ return new Promise((resolve11) => {
251429
+ const request2 = http.get(
251430
+ {
251431
+ host: "127.0.0.1",
251432
+ port,
251433
+ path: "/bridge-token-check",
251434
+ timeout: timeoutMs,
251435
+ headers: { Authorization: `Bearer ${token}` }
251436
+ },
251437
+ (response) => {
251438
+ response.resume();
251439
+ resolve11(response.statusCode !== 401 && response.statusCode !== 403);
251440
+ }
251441
+ );
251442
+ request2.on("timeout", () => {
251443
+ request2.destroy();
251444
+ resolve11(true);
251445
+ });
251446
+ request2.on("error", () => resolve11(true));
251447
+ });
251448
+ }
251366
251449
 
251367
251450
  // src/server/standalone.ts
251368
251451
  init_StdioBridge();
251369
251452
  init_DaemonAuthToken();
251453
+ init_AgentToken();
251370
251454
  init_ConfigLoader();
251371
251455
 
251372
251456
  // src/server/cli/subcommands.ts
@@ -253035,12 +253119,12 @@ async function main() {
253035
253119
  const configDir = defaultDaemonConfigDir();
253036
253120
  const { address, source, record: record2 } = resolveDaemonAddress({ args, env: process.env, configDir });
253037
253121
  if (address) {
253038
- const expected = record2 ? {
253039
- pid: record2.pid,
253040
- startedAt: record2.startedAt,
253041
- token: readDaemonAuthToken(configDir)
253042
- } : void 0;
253043
- const probe = await probeSharedDaemon(address.port, 500, expected);
253122
+ const accessToken = readDaemonAuthToken(configDir);
253123
+ const expected = record2 ? { pid: record2.pid, startedAt: record2.startedAt, token: accessToken } : accessToken ? { token: accessToken } : void 0;
253124
+ const probed = await probeSharedDaemon(address.port, 500, expected);
253125
+ const agentToken = readAgentToken(configDir);
253126
+ const foreignReason = !probed.identity ? void 0 : !agentToken ? "no-agent-token" : await isTokenAccepted(address.port, agentToken, 500) ? void 0 : "agent-token-rejected";
253127
+ const probe = foreignReason ? { attempts: probed.attempts, failureReason: foreignReason } : probed;
253044
253128
  if (probe.identity) {
253045
253129
  console.error(
253046
253130
  `[grain] bridging stdio to shared daemon v${probe.identity.version ?? "?"} on ${address.host}:${address.port} (via ${source}, RFC-082) after ${probe.attempts} attempt(s)`
@@ -253050,8 +253134,9 @@ async function main() {
253050
253134
  return;
253051
253135
  }
253052
253136
  const reasonStr = probe.failureReason === "stale-record" ? `stale-record pid ${expected?.pid}` : probe.failureReason;
253137
+ const detail = probe.failureReason === "no-agent-token" ? `answered, but ${configDir} has no agent-token for this bridge to present` : probe.failureReason === "wrong-instance" || probe.failureReason === "agent-token-rejected" ? `answered, but it does not accept the tokens in ${configDir}, so it belongs to a different Grain home` : void 0;
253053
253138
  console.error(
253054
- `[grain] no shared daemon at ${address.host}:${address.port} (via ${source}) after ${probe.attempts} attempt(s) (${reasonStr}); starting embedded stdio daemon \u2014 this session's queries will not appear in the extension's results panel`
253139
+ (detail ? `[grain] the daemon at ${address.host}:${address.port} (via ${source}) ${detail} (${reasonStr}); check GRAIN_HOME in this MCP server's config. Starting embedded stdio ` : `[grain] no shared daemon at ${address.host}:${address.port} (via ${source}) after ${probe.attempts} attempt(s) (${reasonStr}); starting embedded stdio `) + `daemon \u2014 this session's queries will not appear in the extension's results panel`
253055
253140
  );
253056
253141
  notSetUpMessage = resolveNotSetUpMessage(Boolean(process.env["GRAIN_CONNECTIONS"]));
253057
253142
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grain-tools",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
4
4
  "mcpName": "io.github.pattrnlabs/grain",
5
5
  "description": "Standalone Grain MCP database server for VS Code, Claude Desktop, Cursor, and other MCP clients.",
6
6
  "main": "out/server/standalone.js",
package/server.json CHANGED
@@ -8,14 +8,14 @@
8
8
  "source": "github",
9
9
  "id": "1348511592"
10
10
  },
11
- "version": "0.10.12",
11
+ "version": "0.10.13",
12
12
  "websiteUrl": "https://grain.tools",
13
13
  "packages": [
14
14
  {
15
15
  "registryType": "npm",
16
16
  "registryBaseUrl": "https://registry.npmjs.org",
17
17
  "identifier": "grain-tools",
18
- "version": "0.10.12",
18
+ "version": "0.10.13",
19
19
  "runtimeHint": "npx",
20
20
  "runtimeArguments": [
21
21
  {