@sonenta/mcp 0.36.0 → 0.37.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Model Context Protocol server for **Sonenta** translation management — **pure TypeScript**, no Python, no `uv`, no bundled wheel. Runs on Node ≥18.
4
4
 
5
- Wires Claude Desktop and other MCP clients into your Sonenta project's keys, translations, accessibility surfaces, source-health, and glossary tools.
5
+ Wires Claude Desktop and other MCP clients into your Sonenta project's keys, translations, accessibility surfaces, source-health, and glossary tools — plus **agent-observability** tools (`session_start` / `annotate` / `human_needed` / `session_end`) that surface an agent's run as a live, timestamped thread in the Sonenta dashboard.
6
6
 
7
7
  ## Use
8
8
 
@@ -8388,21 +8388,21 @@ var init_protocol = __esm({
8388
8388
  * the error appropriately (e.g., by failing the task, logging, etc.). The Protocol layer
8389
8389
  * simply propagates the error.
8390
8390
  */
8391
- async _enqueueTaskMessage(taskId, message, sessionId) {
8391
+ async _enqueueTaskMessage(taskId, message, sessionId2) {
8392
8392
  if (!this._taskStore || !this._taskMessageQueue) {
8393
8393
  throw new Error("Cannot enqueue task message: taskStore and taskMessageQueue are not configured");
8394
8394
  }
8395
8395
  const maxQueueSize = this._options?.maxTaskQueueSize;
8396
- await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize);
8396
+ await this._taskMessageQueue.enqueue(taskId, message, sessionId2, maxQueueSize);
8397
8397
  }
8398
8398
  /**
8399
8399
  * Clears the message queue for a task and rejects any pending request resolvers.
8400
8400
  * @param taskId The task ID whose queue should be cleared
8401
8401
  * @param sessionId Optional session ID for binding the operation to a specific session
8402
8402
  */
8403
- async _clearTaskQueue(taskId, sessionId) {
8403
+ async _clearTaskQueue(taskId, sessionId2) {
8404
8404
  if (this._taskMessageQueue) {
8405
- const messages = await this._taskMessageQueue.dequeueAll(taskId, sessionId);
8405
+ const messages = await this._taskMessageQueue.dequeueAll(taskId, sessionId2);
8406
8406
  for (const message of messages) {
8407
8407
  if (message.type === "request" && isJSONRPCRequest(message.message)) {
8408
8408
  const requestId = message.message.id;
@@ -8445,7 +8445,7 @@ var init_protocol = __esm({
8445
8445
  }, { once: true });
8446
8446
  });
8447
8447
  }
8448
- requestTaskStore(request, sessionId) {
8448
+ requestTaskStore(request, sessionId2) {
8449
8449
  const taskStore = this._taskStore;
8450
8450
  if (!taskStore) {
8451
8451
  throw new Error("No task store configured");
@@ -8458,18 +8458,18 @@ var init_protocol = __esm({
8458
8458
  return await taskStore.createTask(taskParams, request.id, {
8459
8459
  method: request.method,
8460
8460
  params: request.params
8461
- }, sessionId);
8461
+ }, sessionId2);
8462
8462
  },
8463
8463
  getTask: async (taskId) => {
8464
- const task = await taskStore.getTask(taskId, sessionId);
8464
+ const task = await taskStore.getTask(taskId, sessionId2);
8465
8465
  if (!task) {
8466
8466
  throw new McpError(ErrorCode.InvalidParams, "Failed to retrieve task: Task not found");
8467
8467
  }
8468
8468
  return task;
8469
8469
  },
8470
8470
  storeTaskResult: async (taskId, status, result) => {
8471
- await taskStore.storeTaskResult(taskId, status, result, sessionId);
8472
- const task = await taskStore.getTask(taskId, sessionId);
8471
+ await taskStore.storeTaskResult(taskId, status, result, sessionId2);
8472
+ const task = await taskStore.getTask(taskId, sessionId2);
8473
8473
  if (task) {
8474
8474
  const notification = TaskStatusNotificationSchema.parse({
8475
8475
  method: "notifications/tasks/status",
@@ -8482,18 +8482,18 @@ var init_protocol = __esm({
8482
8482
  }
8483
8483
  },
8484
8484
  getTaskResult: (taskId) => {
8485
- return taskStore.getTaskResult(taskId, sessionId);
8485
+ return taskStore.getTaskResult(taskId, sessionId2);
8486
8486
  },
8487
8487
  updateTaskStatus: async (taskId, status, statusMessage) => {
8488
- const task = await taskStore.getTask(taskId, sessionId);
8488
+ const task = await taskStore.getTask(taskId, sessionId2);
8489
8489
  if (!task) {
8490
8490
  throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found - it may have been cleaned up`);
8491
8491
  }
8492
8492
  if (isTerminal(task.status)) {
8493
8493
  throw new McpError(ErrorCode.InvalidParams, `Cannot update task "${taskId}" from terminal status "${task.status}" to "${status}". Terminal states (completed, failed, cancelled) cannot transition to other states.`);
8494
8494
  }
8495
- await taskStore.updateTaskStatus(taskId, status, statusMessage, sessionId);
8496
- const updatedTask = await taskStore.getTask(taskId, sessionId);
8495
+ await taskStore.updateTaskStatus(taskId, status, statusMessage, sessionId2);
8496
+ const updatedTask = await taskStore.getTask(taskId, sessionId2);
8497
8497
  if (updatedTask) {
8498
8498
  const notification = TaskStatusNotificationSchema.parse({
8499
8499
  method: "notifications/tasks/status",
@@ -8506,7 +8506,7 @@ var init_protocol = __esm({
8506
8506
  }
8507
8507
  },
8508
8508
  listTasks: (cursor) => {
8509
- return taskStore.listTasks(cursor, sessionId);
8509
+ return taskStore.listTasks(cursor, sessionId2);
8510
8510
  }
8511
8511
  };
8512
8512
  }
@@ -15724,8 +15724,8 @@ var init_server2 = __esm({
15724
15724
  this._serverInfo = _serverInfo;
15725
15725
  this._loggingLevels = /* @__PURE__ */ new Map();
15726
15726
  this.LOG_LEVEL_SEVERITY = new Map(LoggingLevelSchema.options.map((level, index) => [level, index]));
15727
- this.isMessageIgnored = (level, sessionId) => {
15728
- const currentLevel = this._loggingLevels.get(sessionId);
15727
+ this.isMessageIgnored = (level, sessionId2) => {
15728
+ const currentLevel = this._loggingLevels.get(sessionId2);
15729
15729
  return currentLevel ? this.LOG_LEVEL_SEVERITY.get(level) < this.LOG_LEVEL_SEVERITY.get(currentLevel) : false;
15730
15730
  };
15731
15731
  this._capabilities = options?.capabilities ?? {};
@@ -16068,9 +16068,9 @@ var init_server2 = __esm({
16068
16068
  * @param params
16069
16069
  * @param sessionId optional for stateless and backward compatibility
16070
16070
  */
16071
- async sendLoggingMessage(params, sessionId) {
16071
+ async sendLoggingMessage(params, sessionId2) {
16072
16072
  if (this._capabilities.logging) {
16073
- if (!this.isMessageIgnored(params.level, sessionId)) {
16073
+ if (!this.isMessageIgnored(params.level, sessionId2)) {
16074
16074
  return this.notification({ method: "notifications/message", params });
16075
16075
  }
16076
16076
  }
@@ -16904,6 +16904,19 @@ var init_client = __esm({
16904
16904
  this.config = config3;
16905
16905
  }
16906
16906
  config;
16907
+ /**
16908
+ * Active agent-observability session (contract §0). Set by `session_start`;
16909
+ * while non-null it is attached as `X-MCP-Session-Id` to EVERY subsequent MCP
16910
+ * request so the backend can auto-attach captured mutations to the thread.
16911
+ * Cleared by `session_end` so later calls don't bind to a closed session.
16912
+ */
16913
+ sessionId = null;
16914
+ setSession(id) {
16915
+ this.sessionId = id;
16916
+ }
16917
+ clearSession() {
16918
+ this.sessionId = null;
16919
+ }
16907
16920
  buildUrl(path, params) {
16908
16921
  const base = this.config.apiBase.replace(/\/+$/, "");
16909
16922
  const url = new URL(`${base}${path}`);
@@ -16927,6 +16940,7 @@ var init_client = __esm({
16927
16940
  "User-Agent": this.config.userAgent,
16928
16941
  Accept: "application/json"
16929
16942
  });
16943
+ if (this.sessionId) headers.set("X-MCP-Session-Id", this.sessionId);
16930
16944
  const init = { method, headers };
16931
16945
  if (opts.json !== void 0) {
16932
16946
  headers.set("Content-Type", "application/json");
@@ -19429,6 +19443,125 @@ var init_schemas_generated = __esm({
19429
19443
  }
19430
19444
  });
19431
19445
 
19446
+ // src/tools/schemas.observability.ts
19447
+ var PROJECT_UUID, SESSION_ID, OBSERVABILITY_SCHEMAS, OBSERVABILITY_TOOL_NAMES;
19448
+ var init_schemas_observability = __esm({
19449
+ "src/tools/schemas.observability.ts"() {
19450
+ "use strict";
19451
+ PROJECT_UUID = {
19452
+ type: "string",
19453
+ description: "Project UUID. Required when multiple projects are configured (SONENTA_PROJECTS); defaults to the single configured project otherwise."
19454
+ };
19455
+ SESSION_ID = {
19456
+ type: "string",
19457
+ description: "Agent session id returned by session_start. Optional: defaults to this server's active session, so you normally omit it after calling session_start."
19458
+ };
19459
+ OBSERVABILITY_SCHEMAS = {
19460
+ session_start: {
19461
+ description: "Open an observability session so your work shows up as a live, timestamped thread in the Sonenta dashboard. Call this ONCE at the very start of a task, before touching anything. After it returns, every later MCP call you make is automatically attached to this session's thread (no extra wiring) \u2014 so real mutations (keys created, translations proposed, variants set\u2026) are captured even if you never call annotate. Returns {session_id, status, channel}. Idempotent per (project, session_ref) while a running session with that ref exists. Observability is always free (0 credits).",
19462
+ inputSchema: {
19463
+ type: "object",
19464
+ properties: {
19465
+ project_uuid: PROJECT_UUID,
19466
+ agent: {
19467
+ type: "string",
19468
+ description: "Stable agent label shown in the dashboard thread, e.g. 'sonenta-source-health' or 'sonenta-a11y'. Use your own agent name."
19469
+ },
19470
+ title: {
19471
+ type: "string",
19472
+ description: "Optional one-line human-readable title for this run, e.g. 'Repair placeholder drift'."
19473
+ },
19474
+ session_ref: {
19475
+ type: "string",
19476
+ description: "Optional client idempotency key; reusing it while a session is still running returns that same session."
19477
+ },
19478
+ metadata: {
19479
+ type: "object",
19480
+ description: "Optional opaque object echoed back in the session detail (free-form run context)."
19481
+ }
19482
+ },
19483
+ required: ["agent"]
19484
+ }
19485
+ },
19486
+ annotate: {
19487
+ description: "Append one readable line to the current session's thread \u2014 a human-facing step summary, NOT structured data. Use it to narrate what you are doing at a level a project owner can follow ('Found 12 keys with fr placeholder drift; repairing'). Optional; the thread stays faithful without it because real mutations are auto-captured. Free (0 credits).",
19488
+ inputSchema: {
19489
+ type: "object",
19490
+ properties: {
19491
+ project_uuid: PROJECT_UUID,
19492
+ session_id: SESSION_ID,
19493
+ summary: {
19494
+ type: "string",
19495
+ description: "The readable one-line step summary to append to the thread."
19496
+ },
19497
+ level: {
19498
+ type: "string",
19499
+ enum: ["info", "warning"],
19500
+ default: "info",
19501
+ description: "info for normal progress; warning to flag something the human should notice."
19502
+ },
19503
+ payload: {
19504
+ type: "object",
19505
+ description: "Optional structured detail rendered when the line is expanded in the dashboard."
19506
+ }
19507
+ },
19508
+ required: ["summary"]
19509
+ }
19510
+ },
19511
+ human_needed: {
19512
+ description: "Raise a decision you are waiting on a human for \u2014 it appears as a todo in the dashboard and pauses that branch of work until a person resolves it. Use this the moment you hit a choice that is the founder's to make (never guess a business rule): merge vs keep-separate, an ambiguous source, an irreversible action. Provide clear options when there is a discrete set. Free (0 credits).",
19513
+ inputSchema: {
19514
+ type: "object",
19515
+ properties: {
19516
+ project_uuid: PROJECT_UUID,
19517
+ session_id: SESSION_ID,
19518
+ question: {
19519
+ type: "string",
19520
+ description: `The decision, phrased for a human, e.g. "Merge duplicate 'Save' keys or keep them separate?".`
19521
+ },
19522
+ detail: {
19523
+ type: "string",
19524
+ description: "Optional context that helps the human decide."
19525
+ },
19526
+ deep_link: {
19527
+ type: "string",
19528
+ description: "Optional relative dashboard path to the thing needing attention; defaults to the session thread."
19529
+ },
19530
+ options: {
19531
+ type: "array",
19532
+ items: { type: "string" },
19533
+ description: "Optional suggested choices, e.g. ['merge', 'keep separate']."
19534
+ }
19535
+ },
19536
+ required: ["question"]
19537
+ }
19538
+ },
19539
+ session_end: {
19540
+ description: "Close the current observability session when the task is done (or aborted). Records a closing line, marks the thread ended, and triggers the opt-in end-of-session email to project devs. After this, later MCP calls no longer attach to this session. Idempotent \u2014 ending an already-ended session is a no-op. Free (0 credits).",
19541
+ inputSchema: {
19542
+ type: "object",
19543
+ properties: {
19544
+ project_uuid: PROJECT_UUID,
19545
+ session_id: SESSION_ID,
19546
+ summary: {
19547
+ type: "string",
19548
+ description: "Optional closing line, e.g. 'Repaired 12 keys, 1 decision pending'."
19549
+ },
19550
+ status: {
19551
+ type: "string",
19552
+ enum: ["ended", "aborted"],
19553
+ default: "ended",
19554
+ description: "ended for a normal finish; aborted if you stopped early."
19555
+ }
19556
+ },
19557
+ required: []
19558
+ }
19559
+ }
19560
+ };
19561
+ OBSERVABILITY_TOOL_NAMES = Object.keys(OBSERVABILITY_SCHEMAS);
19562
+ }
19563
+ });
19564
+
19432
19565
  // src/tools/registry.ts
19433
19566
  var registry_exports = {};
19434
19567
  __export(registry_exports, {
@@ -19441,6 +19574,14 @@ function copyPresent(body, args, keys) {
19441
19574
  function copyTruthy(target, args, keys) {
19442
19575
  for (const k of keys) if (args[k]) target[k] = args[k];
19443
19576
  }
19577
+ function sessionId(args, client) {
19578
+ const explicit = args["session_id"];
19579
+ if (explicit) return str(explicit);
19580
+ if (client.sessionId) return client.sessionId;
19581
+ throw new ToolValidationError(
19582
+ "session_id is required (call session_start first, or pass session_id explicitly)"
19583
+ );
19584
+ }
19444
19585
  function a11yReportParams(args) {
19445
19586
  const params = {};
19446
19587
  if (args["version_id"]) params["version_id"] = str(args["version_id"]);
@@ -19480,6 +19621,7 @@ var init_registry = __esm({
19480
19621
  init_errors3();
19481
19622
  init_helpers2();
19482
19623
  init_schemas_generated();
19624
+ init_schemas_observability();
19483
19625
  A11Y_SURFACES = ["aria_label", "alt_text", "screen_reader", "plain_language"];
19484
19626
  has = (args, k) => k in args;
19485
19627
  truthy = (args, k) => Boolean(args[k]);
@@ -19997,9 +20139,48 @@ var init_registry = __esm({
19997
20139
  let url = `/v1/mcp/projects/${p}/source-duplicates/propose-plan`;
19998
20140
  if (args["version_id"]) url += `?version_id=${str(args["version_id"])}`;
19999
20141
  return client.post(url, body);
20142
+ },
20143
+ // ---- agent observability (contract v1, task 1611) ----
20144
+ async session_start(args, client) {
20145
+ const p = resolveProjectUuid(args, client);
20146
+ if (!truthy(args, "agent")) throw new ToolValidationError("agent is required");
20147
+ const body = { agent: args["agent"] };
20148
+ copyPresent(body, args, ["title", "session_ref", "metadata"]);
20149
+ const data = await client.post(`/v1/mcp/projects/${p}/agent/sessions`, body);
20150
+ const sid = data?.["session_id"];
20151
+ if (typeof sid === "string" && sid) client.setSession(sid);
20152
+ return data;
20153
+ },
20154
+ async annotate(args, client) {
20155
+ const p = resolveProjectUuid(args, client);
20156
+ const sid = sessionId(args, client);
20157
+ if (!truthy(args, "summary")) throw new ToolValidationError("summary is required");
20158
+ const body = { summary: args["summary"] };
20159
+ copyPresent(body, args, ["level", "payload"]);
20160
+ return client.post(`/v1/mcp/projects/${p}/agent/sessions/${sid}/annotate`, body);
20161
+ },
20162
+ async human_needed(args, client) {
20163
+ const p = resolveProjectUuid(args, client);
20164
+ const sid = sessionId(args, client);
20165
+ if (!truthy(args, "question")) throw new ToolValidationError("question is required");
20166
+ const body = { question: args["question"] };
20167
+ copyPresent(body, args, ["detail", "deep_link", "options"]);
20168
+ return client.post(`/v1/mcp/projects/${p}/agent/sessions/${sid}/human-needed`, body);
20169
+ },
20170
+ async session_end(args, client) {
20171
+ const p = resolveProjectUuid(args, client);
20172
+ const sid = sessionId(args, client);
20173
+ const body = {};
20174
+ copyPresent(body, args, ["summary", "status"]);
20175
+ const data = await client.post(`/v1/mcp/projects/${p}/agent/sessions/${sid}/end`, body);
20176
+ if (client.sessionId === sid) client.clearSession();
20177
+ return data;
20000
20178
  }
20001
20179
  };
20002
- TOOLS = Object.entries(SCHEMAS).map(([name, schema]) => {
20180
+ TOOLS = Object.entries({
20181
+ ...SCHEMAS,
20182
+ ...OBSERVABILITY_SCHEMAS
20183
+ }).map(([name, schema]) => {
20003
20184
  const handler = HANDLERS[name];
20004
20185
  if (!handler) throw new Error(`registry: no handler ported for tool '${name}'`);
20005
20186
  return { name, description: schema.description, inputSchema: schema.inputSchema, handler };
@@ -20089,7 +20270,7 @@ var init_server3 = __esm({
20089
20270
  init_errors3();
20090
20271
  init_helpers2();
20091
20272
  init_registry();
20092
- VERSION = "0.35.0";
20273
+ VERSION = "0.37.0";
20093
20274
  }
20094
20275
  });
20095
20276