@somacheck/vibecheck 0.6.9 → 0.6.11

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/server.js CHANGED
@@ -2,10 +2,22 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { StatementPendingError, } from "./vibecheck.js";
4
4
  import { SomaCheckCompatibilityError, SomaCheckHttpError, SomaCheckLiveAskClientError, SomaCheckLiveAskConflictError, SomaCheckLiveAskPendingError, } from "./api.js";
5
- import { PACKAGE_NAME, PACKAGE_SPEC, PACKAGE_VERSION } from "./constants.js";
6
- const LIVE_ASK_WAIT_BUDGET_MS = 10_000;
5
+ import { PACKAGE_NAME, PACKAGE_SPEC, PACKAGE_VERSION, SOMACHECK_SETUP_URL } from "./constants.js";
6
+ // A human gesture (unlock phone, read the statement, make the wrist gesture)
7
+ // reliably takes longer than the old 10s budget, so an inline wait almost
8
+ // never resolves before the tool returns pending. The MCP SDK's documented
9
+ // default tool-call timeout is 60s (see LIVE-ASK-CONTRACT.md); 45s leaves a
10
+ // 15s margin for token loading, the create round trip, and result-read
11
+ // overhead on top of the sleeps below, while covering most prompt gestures
12
+ // inline. Overridable via `liveAskWait.budgetMs` so tests stay fast.
13
+ const LIVE_ASK_WAIT_BUDGET_MS = 45_000;
14
+ // Initial backoff, then steady 4s polling. The steady tail's length is sized
15
+ // so the sum matches LIVE_ASK_WAIT_BUDGET_MS above — the wait loop below
16
+ // stops as soon as either the array or the deadline is exhausted, so a
17
+ // smaller injected budgetMs (tests) still ends early on the deadline check.
7
18
  const LIVE_ASK_POLL_DELAYS_MS = [
8
19
  1_000, 1_500, 2_500, 4_000,
20
+ 4_000, 4_000, 4_000, 4_000, 4_000, 4_000, 4_000, 4_000, 4_000,
9
21
  ];
10
22
  const CLAUDE_CHANNEL_POLL_INTERVAL_MS = 2_000;
11
23
  const CLAUDE_CHANNEL_MAX_WATCH_MS = 15 * 60 * 1_000;
@@ -59,8 +71,8 @@ const resultSchema = {
59
71
  request_id: z.string(),
60
72
  status: z.enum(["queued", "pending", "answered", "expired", "cancelled"]),
61
73
  verdict: z.enum(["aligned", "unaligned"]).nullable(),
62
- confidence: z.number().nullable(),
63
- latency_s: z.number().nullable(),
74
+ confidence: z.number().min(0).max(1).nullable(),
75
+ latency_s: z.number().min(0).nullable(),
64
76
  user_feedback: z.enum(["agreed", "disagreed", "not_provided"]).nullable().optional(),
65
77
  };
66
78
  const contextItemSchema = z.object({
@@ -70,7 +82,7 @@ const contextItemSchema = z.object({
70
82
  confidence: z.number().min(0).max(1),
71
83
  answered_at: z.string().datetime({ offset: true }),
72
84
  });
73
- const contextSchema = { checkins: z.array(contextItemSchema) };
85
+ const contextSchema = { checkins: z.array(contextItemSchema).max(20) };
74
86
  const liveAskErrorCodeSchema = z.enum([
75
87
  "link_revoked",
76
88
  "upgrade_required",
@@ -107,9 +119,8 @@ const agentContextObservationSchema = z.object({
107
119
  evidence_count: z.number().int().min(1).max(100).default(1),
108
120
  });
109
121
  const contextShareSchema = {
110
- source_id: z.string(), entry_id: z.string(), accepted: z.boolean(),
122
+ accepted: z.boolean(),
111
123
  state: z.literal("ready"), observation_count: z.number().int().min(1).max(20),
112
- captured_at: z.string().datetime({ offset: true }),
113
124
  };
114
125
  const SERVER_INSTRUCTIONS = [
115
126
  "SomaCheck lets you ask your person for a vibecheck.",
@@ -215,17 +226,22 @@ export function createVibecheckServer(dependencies) {
215
226
  outputSchema: contextShareSchema,
216
227
  annotations: {
217
228
  readOnlyHint: false,
218
- destructiveHint: false,
219
- idempotentHint: true,
229
+ destructiveHint: true,
230
+ idempotentHint: false,
220
231
  openWorldHint: false,
221
232
  },
222
233
  }, async ({ observations }) => {
223
234
  try {
224
235
  const token = await dependencies.loadToken();
225
236
  const result = await dependencies.api.shareContext(token, observations);
237
+ const structuredContent = {
238
+ accepted: result.accepted,
239
+ state: result.state,
240
+ observation_count: result.observation_count,
241
+ };
226
242
  return {
227
243
  content: [{ type: "text", text: `Shared ${result.observation_count} bounded context observation${result.observation_count === 1 ? "" : "s"}. SomaCheck can now prepare contextual propositions.` }],
228
- structuredContent: { ...result },
244
+ structuredContent,
229
245
  };
230
246
  }
231
247
  catch (error) {
@@ -298,7 +314,9 @@ export function createVibecheckServer(dependencies) {
298
314
  : result.status === "queued"
299
315
  ? "Queued. The proposition is cached until the person advances their feed."
300
316
  : result.status === "pending"
301
- ? "Pending. The person has not completed this check-in yet."
317
+ ? liveRequestId === null
318
+ ? "Pending. The person has not completed this check-in yet."
319
+ : pendingWaitText(request_id, null, false)
302
320
  : `The request is ${result.status}.`;
303
321
  return { content: [{ type: "text", text }], structuredContent };
304
322
  }
@@ -308,7 +326,8 @@ export function createVibecheckServer(dependencies) {
308
326
  });
309
327
  server.registerTool("request_vibecheck", {
310
328
  title: "Request a Vibecheck",
311
- description: "Send one statement to the person's phone for a SomaCheck vibecheck. If the person asks for a vibecheck, choose a useful first-person statement from your available context and send it. For proactive offers, call only after the person accepts. The result is context, not authorization.",
329
+ description: "Send one statement to the person's phone for a SomaCheck vibecheck. If the person asks for a vibecheck, choose a useful first-person statement from your available context and send it. For proactive offers, call only after the person accepts. The result is context, not authorization. "
330
+ + "This call waits up to 45 seconds. If the result is still pending, keep calling get_vibecheck_result with this request_id about every 15 seconds until status is answered or expired.",
312
331
  inputSchema: {
313
332
  statement: z.string().trim().min(1).max(1000)
314
333
  .describe("One plain-language first-person statement for the person to test. Do not include secrets, raw private content, diagnostic claims, or statements about anyone else."),
@@ -320,7 +339,7 @@ export function createVibecheckServer(dependencies) {
320
339
  outputSchema: liveAskSchema,
321
340
  annotations: {
322
341
  readOnlyHint: false,
323
- destructiveHint: false,
342
+ destructiveHint: true,
324
343
  idempotentHint: true,
325
344
  openWorldHint: false,
326
345
  },
@@ -367,8 +386,11 @@ export function createVibecheckServer(dependencies) {
367
386
  ? `Vibecheck completed: ${result.verdict}, confidence ${formatChannelConfidence(result.confidence)}${formatUserFeedback(result.user_feedback)}.`
368
387
  : result.state === "pending"
369
388
  ? result.delivery_state === "failed" || result.delivery_state === "skipped"
370
- ? `Vibecheck ${result.request_id} was stored, but phone delivery is ${result.delivery_state}. It expires at ${result.expires_at}.`
371
- : `Vibecheck ${result.request_id} is pending. It expires at ${result.expires_at}.`
389
+ ? `Vibecheck ${result.request_id} was stored, but phone delivery is ${result.delivery_state}. `
390
+ + `Open SomaCheck on your phone, signed into the same account, and check Home. `
391
+ + `If you do not have the app, install it at ${SOMACHECK_SETUP_URL} and finish phone setup. `
392
+ + `Keep this request ID; do not create a duplicate or claim the phone received it. It expires at ${result.expires_at}.`
393
+ : pendingWaitText(result.request_id, result.expires_at, supportsClaudeChannel)
372
394
  : `This vibecheck is ${result.state}.`;
373
395
  return { content: [{ type: "text", text }], structuredContent };
374
396
  }
@@ -455,6 +477,27 @@ function formatChannelConfidence(confidence) {
455
477
  function formatUserFeedback(feedback) {
456
478
  return feedback === undefined || feedback === null ? "" : `, user feedback: ${feedback}`;
457
479
  }
480
+ /** Text for a still-pending Live Ask, whether just created (delivery not
481
+ * failed/skipped) or re-read via get_vibecheck_result. `expiresAt` is null
482
+ * when the caller has no expiry to report (get_vibecheck_result's live read
483
+ * does not receive one). `hasLocalWatch` is true only for the local stdio
484
+ * server when the Claude PostToolUse hook's background watch is armed for
485
+ * this request (see startClaudeChannelWatch) — that watch notifies the same
486
+ * session automatically, so the agent should not be told to poll. The hosted
487
+ * edge function is stateless per request and never has a watch, so it must
488
+ * tell the agent exactly what to do next instead of inviting it to ask the
489
+ * human. */
490
+ function pendingWaitText(requestId, expiresAt, hasLocalWatch) {
491
+ if (hasLocalWatch) {
492
+ return `Vibecheck ${requestId} is pending. It expires at ${expiresAt}. This session will be notified automatically when the person responds.`;
493
+ }
494
+ return `Vibecheck ${requestId} is pending. SomaCheck has not received a response yet. `
495
+ + `If no phone prompt appears, open SomaCheck on the same account; phone setup is at ${SOMACHECK_SETUP_URL}. `
496
+ + `Call get_vibecheck_result with request_id ${requestId} again in about 15 seconds, `
497
+ + `and keep polling at that cadence until the state is answered or expired. `
498
+ + `Do not end your turn or ask the person whether to check; they are answering on their phone.`
499
+ + (expiresAt === null ? "" : ` It expires at ${expiresAt}.`);
500
+ }
458
501
  async function waitForLiveVibecheck(created, token, identity, dependencies, signal, deadline, progress) {
459
502
  if (created.state !== "pending"
460
503
  || created.delivery_state === "failed"
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@somacheck/vibecheck",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "mcpName": "io.github.Sensie-agents/vibecheck",
5
5
  "description": "Send a consented vibecheck to SomaCheck and use the result as a signal.",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/sensie-app/Somacheck.git",
9
- "directory": "packages/vibecheck"
8
+ "url": "git+https://github.com/Sensie-agents/vibecheck.git"
10
9
  },
11
10
  "type": "module",
12
11
  "bin": {
@@ -18,6 +17,7 @@
18
17
  "files": [
19
18
  "dist/*.js",
20
19
  "SKILL.md",
20
+ "recipes/**",
21
21
  "claude-marketplace/**"
22
22
  ],
23
23
  "scripts": {
@@ -40,7 +40,7 @@
40
40
  "tsx": "^4.23.1",
41
41
  "typescript": "^5.9.3"
42
42
  },
43
- "license": "UNLICENSED",
43
+ "license": "MIT",
44
44
  "publishConfig": {
45
45
  "access": "public"
46
46
  }
@@ -0,0 +1,323 @@
1
+ ---
2
+ title: Chattermill + SomaCheck Research Reflection
3
+ recipe_id: chattermill-research-reflection-v1
4
+ status: P1 researcher-side read-only reflection; aggregate metrics and generated highlights only
5
+ audience: Researchers and CX analysts using Chattermill with an MCP-capable agent
6
+ updated: 2026-09-05
7
+ required_mcp_servers:
8
+ - chattermill
9
+ - vibecheck
10
+ ---
11
+
12
+ # Chattermill + SomaCheck Research Reflection
13
+
14
+ This recipe gives a Chattermill researcher one private SomaCheck check-in while
15
+ shaping the researcher's own interpretation of customer-feedback insights. The
16
+ researcher stays the only person who reads, decides, or acts. The recipe reads
17
+ only aggregate metrics or generated highlights and performs no Chattermill
18
+ write.
19
+
20
+ Chattermill exposes a hosted read-only MCP surface
21
+ (`https://app.chattermill.com/mcp`, Streamable HTTP). Its published OAuth
22
+ protected-resource metadata advertises only the `mcp:read` scope alongside
23
+ `openid profile email offline_access`; there is no write scope, and the server
24
+ is documented as read-only. Existing Chattermill roles and permissions still
25
+ apply, so an agent never sees data the researcher cannot already view.
26
+
27
+ The SomaCheck reading is context for the researcher's own interpretation or
28
+ next step. It is not truth, diagnosis, authorization, evidence about any
29
+ individual customer, or a research decision. The researcher remains the
30
+ authority.
31
+
32
+ ## Capability and access boundary
33
+
34
+ Chattermill documents the hosted MCP as read-only and OAuth 2.1 authenticated
35
+ at `https://app.chattermill.com/mcp`. A product or integration owner provides
36
+ an organization-authorized connection or partner sandbox that can already
37
+ view the named project's aggregate metrics and highlights. The provisioned MCP
38
+ client completes Chattermill's OAuth flow at `auth.chattermill.com`. Running
39
+ this recipe never asks the researcher to sign up, purchase access, administer
40
+ access, or populate Chattermill data.
41
+
42
+ The MCP server's protected-resource metadata advertises `mcp:read` as its only
43
+ MCP scope. There is no write tool in the published contract. If a write
44
+ surface appears at runtime anyway, this recipe still performs no write.
45
+
46
+ This P1 recipe is researcher-side only. Do not use it to:
47
+
48
+ - ask a customer, respondent, employee, candidate, patient, or student to
49
+ complete a SomaCheck;
50
+ - retrieve individual feedback responses, verbatim comments, source quotes,
51
+ respondent identifiers, contact fields, or any free-text feedback;
52
+ - retrieve observation clusters that surface representative verbatim snippets
53
+ or observation identifiers tied to individual feedback;
54
+ - use a reading for customer targeting, eligibility, payment, ranking,
55
+ churn scoring, authenticity, quality, or performance evaluation;
56
+ - write a SomaCheck reading, confidence, gesture, confirmation, or hidden
57
+ score into Chattermill, a dashboard, a tag, a theme, a highlight, an
58
+ insight, or any analytics record; or
59
+ - create, edit, publish, delete, share, export, or otherwise mutate any
60
+ Chattermill project, theme, attribute, metric, dashboard, or insight.
61
+
62
+ Participant- or customer-facing use stays blocked until a `respondent_private`
63
+ architecture prevents researcher or platform access to individual readings,
64
+ methodology and ethics review is complete, and Sensie gives explicit
65
+ privacy/legal approval. Chattermill MCP access, read-only scope, or participant
66
+ consent alone does not satisfy these gates.
67
+
68
+ ## Experience a researcher can run
69
+
70
+ The researcher holds the phone and selects one existing Chattermill project
71
+ supplied by the workspace owner, then provides one research question. The agent
72
+ reads only that project's aggregate metrics (`get_metrics`) or generated
73
+ highlights (`generate_highlights`), plus the discovery metadata needed to build
74
+ the query (`list_themes`, `list_attributes`, `search_attributes`,
75
+ `list_metric_options`). It never calls `get_feedback` (individual responses)
76
+ or `search_observations` (observation identifiers and representative verbatim
77
+ snippets), and never surfaces individual feedback, source quotes, respondent
78
+ identifiers, or free text. The agent offers two defensible interpretations of
79
+ what the aggregate signal suggests, then one short first-person proposition
80
+ about the researcher's own interpretation or next step. After the researcher's
81
+ typed choice, the agent stops. There is no Chattermill write.
82
+
83
+ ## Starter prompt
84
+
85
+ ```text
86
+ Use Chattermill and SomaCheck to help me reflect on my own interpretation of
87
+ customer-feedback insights for one named Chattermill project. I am the
88
+ researcher and I am holding the phone. The project is:
89
+ [PROJECT NAME OR ID PROVIDED BY THE WORKSPACE OWNER]. Use only the
90
+ preconfigured official hosted read-only Chattermill MCP at
91
+ https://app.chattermill.com/mcp. Read only aggregate metrics (get_metrics) or
92
+ generated highlights (generate_highlights), plus discovery metadata
93
+ (list_themes, list_attributes, search_attributes, list_metric_options) needed
94
+ to build the query. Never call get_feedback or search_observations. Never
95
+ surface individual feedback, source quotes, respondent identifiers, contact
96
+ fields, or free text. Propose two defensible interpretations of what the
97
+ aggregate signal suggests, with the tradeoff for each. Then offer one short
98
+ first-person proposition about my own interpretation or next step and wait
99
+ for my acceptance before any proactive SomaCheck ask. Treat Aligned or
100
+ Unaligned plus confidence as context, not truth, diagnosis, authorization, or
101
+ evidence about any individual customer. Ask what I choose in words. Keep the
102
+ proposition, reading, confidence, and my confirmation out of Chattermill. Do
103
+ not write to Chattermill; the hosted MCP is read-only. If the preconfigured
104
+ connection, mcp:read, or a safe aggregate tool is absent, return the copy-ready
105
+ interpretation and tell the integration owner what capability is missing. Do
106
+ not ask me to sign up, purchase access, administer access, or populate
107
+ Chattermill data.
108
+ ```
109
+
110
+ ## Prerequisites and account access
111
+
112
+ 1. The researcher can complete a normal SomaCheck request through the connected
113
+ `vibecheck` MCP server (`request_vibecheck` and
114
+ `get_vibecheck_result`).
115
+ 2. A product or integration owner has provisioned an authorized organization
116
+ connection or partner sandbox that can already view the named project's
117
+ aggregate metrics and highlights. Existing Chattermill roles and permissions
118
+ apply; the MCP grants no access beyond the provisioned workspace.
119
+ 3. The provisioned MCP client is connected to exactly
120
+ `https://app.chattermill.com/mcp` and authenticated through the OAuth 2.1
121
+ flow Chattermill publishes at `auth.chattermill.com`. Do not substitute a
122
+ different host, path, or non-Chattermill endpoint.
123
+ 4. The workspace owner supplies one named existing Chattermill project (by name
124
+ or ID) whose aggregate metrics or generated highlights are enough for the
125
+ reflection task, with no need for individual responses or verbatim comments.
126
+ The researcher is not responsible for creating or populating it.
127
+ 5. No credential, OAuth token, API key, project ID containing PII, or
128
+ respondent identifier appears in chat, logs, prompts, or the SomaCheck
129
+ proposition.
130
+
131
+ ## Chattermill tool policy
132
+
133
+ Chattermill publishes a hosted, read-only MCP (`mcp:read` scope). It documents
134
+ eight tools in two groups: discovery tools and data tools. The agent must
135
+ inspect the live tool list at runtime and use only the smallest tools that
136
+ return aggregate or generated output. Treat the live schemas as
137
+ runtime-discovered; do not assume undocumented tool names.
138
+
139
+ | Purpose | Allowed surface | Rule |
140
+ | --- | --- | --- |
141
+ | Discover theme/category names | `list_themes` | Metadata only; names configured in the project, never feedback content |
142
+ | Discover filterable attributes | `list_attributes` | Metadata only; attribute names, never feedback values |
143
+ | Discover valid filter values | `search_attributes` | Metadata only for segment attributes (country, product, data source); never enumerate an identifier-bearing attribute such as email or customer ID |
144
+ | Discover metric options | `list_metric_options` | Metadata only; chart types, metrics, frequencies, breakdown options |
145
+ | Read aggregate quantitative data | `get_metrics` | Aggregate NPS, sentiment, volume, or trend metrics only |
146
+ | Read generated highlights | `generate_highlights` | AI-generated summary of themes, issues, or trends only |
147
+ | Read individual feedback responses | `get_feedback` | Prohibited in this recipe |
148
+ | Search observation clusters with snippets | `search_observations` | Prohibited in this recipe; returns observation identifiers and representative verbatim snippets |
149
+ | Any create, edit, publish, delete, share, export, or mutation | any write or mutation tool | Prohibited in this recipe |
150
+ | Private reflection | `request_vibecheck`, optional `get_vibecheck_result` | One decision, one request, one stable pending handle |
151
+
152
+ Do not broaden a connection to a root or all-tools endpoint to make this
153
+ recipe work. If the runtime does not expose `get_metrics` or
154
+ `generate_highlights`, label the Chattermill half untested and stop.
155
+
156
+ ## Exact cross-MCP workflow
157
+
158
+ 1. **Confirm the subject and scope.** State that the researcher is the phone
159
+ holder and the subject of the proposition. Name the one existing Chattermill
160
+ project supplied by the workspace owner (by name or ID) and the research
161
+ question.
162
+ 2. **Verify access honestly.** Confirm the client is connected to exactly
163
+ `https://app.chattermill.com/mcp` through Chattermill's OAuth flow and
164
+ that the `mcp:read` scope is present. Do not claim a live pass on
165
+ documentation, a fixture, or a connection that lacks the read scope.
166
+ 3. **Read only aggregate or generated output.** Call `get_metrics` or
167
+ `generate_highlights`, using discovery metadata (`list_themes`,
168
+ `list_attributes`, `search_attributes`, `list_metric_options`) only to
169
+ build the query. Do not call `get_feedback` or `search_observations`, and
170
+ do not surface individual feedback, source quotes, respondent identifiers,
171
+ contact fields, or free text.
172
+ 4. **Separate evidence from interpretation.** Label what the aggregate
173
+ metrics or highlights literally contain, what the agent infers, and what
174
+ the researcher's own judgment must still decide.
175
+ 5. **Propose two interpretations.** Offer two defensible interpretations of
176
+ what the aggregate signal suggests, with the tradeoff for each.
177
+ 6. **Choose one first-person proposition.** It must be short, first-person,
178
+ and about the researcher's own interpretation or next step. Do not include
179
+ project IDs containing PII, individual feedback, source quotes,
180
+ respondent identifiers, contact fields, or free text.
181
+ 7. **Establish one-ask consent.** If the researcher explicitly requested a
182
+ SomaCheck check-in in the current message, use
183
+ `consent_basis: "user_requested_vibecheck"`. Otherwise show the exact
184
+ proposition, wait for acceptance, and use
185
+ `consent_basis: "user_approved_statement"`.
186
+ 8. **Request once.** Call `request_vibecheck` with the approved statement and
187
+ a fresh UUID `idempotency_key`. Reuse it only to retry the identical
188
+ request after an ambiguous create failure.
189
+ 9. **Handle the real lifecycle.** A terminal response from
190
+ `request_vibecheck` uses `state: "completed"`; a later read uses
191
+ `status: "answered"`. If the initial response is `pending`, retain the
192
+ returned `live:<uuid>` handle and call `get_vibecheck_result` with it once
193
+ later. If it is still `pending`, `expired`, `cancelled`, or errors,
194
+ continue without a result. Never issue a replacement ask.
195
+ 10. **Return authority.** Separate observation (a valid gesture was
196
+ captured), interpretation (`aligned` or `unaligned` plus model
197
+ confidence), confirmation (what the researcher says), and choice (what the
198
+ researcher decides). `unaligned` may indicate possible inner conflict
199
+ relative to the proposition; it does not name a cause, choose an
200
+ interpretation, or justify a research claim.
201
+ 11. **Ask for words.** Ask the researcher which interpretation they choose,
202
+ whether they want a third option, or whether they want to stop. Follow
203
+ their typed choice.
204
+ 12. **Keep the signal out of Chattermill.** Do not call any write, mutation,
205
+ export, tag, theme, highlight, insight, or analytics tool. Do not store
206
+ the proposition, reading, confidence, confirmation, or gesture metadata
207
+ in Chattermill.
208
+ 13. **End the test.** Report the exact Chattermill tool names used and the
209
+ exact aggregate or generated fields returned, or report the copy-ready
210
+ interpretation when the Chattermill half is untested. Do not create,
211
+ edit, publish, delete, share, or export any Chattermill object.
212
+
213
+ ## Proposition examples
214
+
215
+ Allowed:
216
+
217
+ - "I can interpret this aggregate signal without reading individual
218
+ feedback."
219
+ - "I have a clear reading of what these aggregate metrics suggest."
220
+ - "I am ready to act on generated highlights without opening verbatim
221
+ comments."
222
+ - "I want to keep individual responses out of my interpretation."
223
+ - "I can state what this trend does and does not show."
224
+
225
+ Not allowed:
226
+
227
+ - "This customer is telling the truth."
228
+ - "These respondents are engaged."
229
+ - "This feedback proves the feature is broken."
230
+ - "This customer should be flagged or deprioritized."
231
+ - "This aggregate metric validates my hypothesis."
232
+
233
+ ## Visible success condition
234
+
235
+ The test passes when:
236
+
237
+ - the researcher selects one existing Chattermill project supplied by a
238
+ workspace owner whose provisioned connection can view its aggregate metrics
239
+ and highlights;
240
+ - the agent reads only aggregate metrics (`get_metrics`) or generated
241
+ highlights (`generate_highlights`) through the hosted read-only
242
+ Chattermill MCP and reports the exact tool names used;
243
+ - the agent never calls `get_feedback` or `search_observations`, and never
244
+ surfaces individual feedback, source quotes, respondent identifiers,
245
+ contact fields, or free text;
246
+ - the researcher receives at most one optional SomaCheck check-in on their
247
+ phone on an exact first-person proposition about their own interpretation or
248
+ next step;
249
+ - the agent presents the result as context, asks the researcher to state their
250
+ choice in words, and follows the typed choice;
251
+ - the agent makes no Chattermill write, mutation, export, share, tag,
252
+ comment, or analytics call; and
253
+ - no individual customer, respondent, or employee data enters the cross-MCP
254
+ flow.
255
+
256
+ ## Failure and degraded paths
257
+
258
+ - **Read scope absent or OAuth failure:** Report the missing capability to the
259
+ product or integration owner; never request a token or API key in chat or ask
260
+ the researcher to sign up, purchase access, or administer access. Label the
261
+ Chattermill half untested, return the copy-ready interpretation, and stop.
262
+ - **Runtime exposes only individual-response or observation tools:** Do not
263
+ call them. Label the Chattermill half untested and stop. Do not broaden
264
+ the connection to make this recipe work.
265
+ - **MCP returns individual feedback, source quotes, respondent identifiers,
266
+ contact fields, or free text:** Stop immediately. Discard the individual
267
+ content from the agent context. Report the exact fields received and ask the
268
+ product or integration owner to revoke or limit the scope. Do not use the
269
+ individual content in any proposition, framing, or interpretation.
270
+ - **SomaCheck unresolved:** Use the same `live:<uuid>` handle once later,
271
+ then proceed without a reading if still unresolved.
272
+ - **Unreadable capture:** Offer a retry only if the researcher wants it;
273
+ unreadable is not a third interpretation.
274
+ - **Researcher disagrees with the reading:** Follow the researcher's typed
275
+ choice without reconciliation or repetition.
276
+
277
+ ## Privacy boundary
278
+
279
+ - Raw phone motion never reaches Chattermill or the agent.
280
+ - Chattermill receives no proposition, reading, confidence, confirmation,
281
+ gesture metadata, or SomaCheck output through this recipe.
282
+ - SomaCheck receives no Chattermill project ID containing PII, individual
283
+ feedback, source quote, respondent identifier, contact field, free-text
284
+ comment, or verbatim response.
285
+ - No individual customer, respondent, or employee data is read into the agent
286
+ conversation, prompt, proposition, framing, or memory.
287
+ - The Chattermill project remains unchanged. No write, mutation, export,
288
+ share, tag, comment, insight, or analytics call occurs.
289
+
290
+ ## Test checklist
291
+
292
+ - [ ] The researcher is the phone holder and the subject of the proposition.
293
+ - [ ] The client is connected to exactly `https://app.chattermill.com/mcp`
294
+ through Chattermill's OAuth 2.1 flow; no other host, path, or
295
+ non-Chattermill endpoint is used.
296
+ - [ ] The `mcp:read` scope is present and no write scope is assumed.
297
+ - [ ] No Chattermill credential, OAuth token, or API key appears in chat,
298
+ logs, or the SomaCheck proposition.
299
+ - [ ] The agent reads only `get_metrics` or `generate_highlights`, using
300
+ discovery metadata only to build the query, and reports the exact
301
+ runtime tool names used.
302
+ - [ ] The agent does not call `get_feedback` or `search_observations`.
303
+ - [ ] No individual feedback, source quote, respondent identifier, contact
304
+ field, or free text appears in the conversation, proposition, or
305
+ interpretation.
306
+ - [ ] The proposition is first-person and contains no Chattermill content.
307
+ - [ ] At most one SomaCheck request is created for the choice.
308
+ - [ ] Observation, interpretation, confirmation, and choice remain separate.
309
+ - [ ] The researcher states a choice in words and the agent follows the typed choice.
310
+ - [ ] The agent makes no Chattermill write, mutation, export, share, tag,
311
+ comment, or analytics call.
312
+ - [ ] No individual customer, respondent, or employee data enters the
313
+ cross-MCP flow.
314
+
315
+ ## Sources
316
+
317
+ - [Chattermill MCP server guide](https://docs.chattermill.com/en/articles/13943134-chattermill-mcp-server)
318
+ - [Chattermill MCP endpoint](https://app.chattermill.com/mcp)
319
+ - [Chattermill MCP protected-resource metadata](https://app.chattermill.com/.well-known/oauth-protected-resource/mcp)
320
+ - [Chattermill MCP authorization-server metadata](https://app.chattermill.com/.well-known/oauth-authorization-server)
321
+ - [SomaCheck MCP setup and tool behavior](../README.md)
322
+ - [SomaCheck immediate-request contract](../LIVE-ASK-CONTRACT.md)
323
+ - [MCP Customer-Testable Experience Catalog](../../../docs/agent-adoption/customer-experience-catalog.md)