Skip to content

Add Poppy discovery and read-only MCP tools using SDKs - #44

Open
shrey150 wants to merge 6 commits into
mainfrom
codex/bell-poppy-config-20261112
Open

shrey150 wants to merge 6 commits into
mainfrom
codex/bell-poppy-config-20261112

Conversation

@shrey150

@shrey150 shrey150 commented Nov 13, 2026 •

Copy link
Copy Markdown

Bell can discover a company's /.well-known/poppy.json, inspect its MCP tools, and perform explicitly read-only operations using published clients.

  • @datalayer/personal-agent-protocol owns discovery, protocol versions, Session signing, device authorization, resource-bound tokens, refresh, and revocation.
  • The official MCP client owns transport, negotiation, pagination, and schema validation.
  • Bell supplies agent tools, public client metadata/JWKS, authenticated owner isolation, approval requirements, encrypted SDK storage, and output limits. Authentication is handled server-side; users complete consent on the provider's website. Bell filters literal echoes of credentials held for the current request, without guessing secrets from field names or token patterns. Unknown or transformed secrets can pass through; this is not a PII or prompt-injection detector.

Compatibility and setup

The pinned Poppy SDK uses 0.4 by default. Providers serving 0.3 are unsupported; Bell contains no protocol translation or compatibility patches. This integration exposes MCP reads only. OpenAPI execution, writes, purchases, and browser handoff are excluded. Only uniquely named tools with readOnlyHint: true and without destructiveHint: true are exposed. These annotations are provider claims, not verified behavior: use providers trusted with the requested data. Bell does not infer permissions from tool or parameter names.

The encrypted file store requires one agent process with durable storage. SDK-provided locks serialize each owner/company independently; short file checkpoints reread, merge, and atomically save current state without holding a file-wide lock during provider requests. Setup is in docs/poppy.md; the repeatable production smoke is in docs/provider-tests.md.

E2E Test Matrix

Path Evidence Limit of this verification
Production model → Bell/Eve → public Poppy demo Rebuilt and passed again after simplifying credential filtering against the live hotel sandbox: four successful calls covered discovery, API inspection, availability, and rates. The answer matched the returned dates, room, rate, raw total/currency, and citation, and identified the data as a demo. Public identity, credential non-disclosure, and session retirement checks passed. Demonstrates the complete model/tool flow using public sandbox data. It does not verify real inventory, booking quotes, authenticated account conversations, or phone delivery.
Noninteractive conversation API A scripted three-message conversation compared four rooms, selected the cheapest room, fetched its cheapest refundable rate, then changed checkout to a three-night stay and fetched the updated rate. Assertions compared dates, room, rate, raw totals/currency, and citations against the actual provider tool payloads. All three turns passed. Verifies real model decisions, retained conversation context, corrections, and task completion against public sandbox data. No stubs or direct tool invocation.
Noninteractive terminal demo Piped stdin completed the hotel request plus a follow-up in 23.1 seconds: five successful tool calls and two answers. A separate --once flower query completed three tool calls in 12 seconds. Both exited 0 and confirmed session retirement. Confirms the runnable demo works without human input. Flower evidence checks tool success and the answer; raw-payload comparison was performed for the hotel conversation.
Scripted approval denial and access control The account request paused at explicit sites:read approval. A scripted Cancel produced a persisted denied event, no successful connection or sign-in result, and a normal agent reply. Unauthenticated session creation returned HTTP 401. Verifies denial and authenticated entry; it does not prove an approved account conversation.
Real provider account access Human-approved Stacktree sites:read passed through Bell's SDK-backed client: API inspection, two successful list_sites reads, and SDK disconnect. Both reads returned an empty list. Direct client verification; no full model conversation or retrieval of populated account data was demonstrated.
Account lifecycle Actual SDKs passed controlled HTTP provider tests for persisted device authorization, owner isolation, resource-bound refresh, token rotation/revocation, interrupted-mutation recovery, approvals, and filtering of current-request credential echoes. Regression tests verify failed refresh → explicit disconnect → fresh device sign-in, independent-account progress during a held provider request, same-account serialization, and preservation of concurrent file updates. Controlled fixtures establish tested behavior; they do not establish compatibility with every live provider.
Linq/iMessage Automated tests cover signed inbound HTTP requests, sender checks, dispatch, and same-chat routing with provider calls and Eve dispatch mocked. No actual phone → Linq → Bell → Poppy → phone round trip has been verified.
Dependency audit and compatibility pnpm audit --prod reports zero known vulnerabilities after a scoped override updates Reactor’s transitive TOON package to 2.3.1. Thirty-two temporary compatibility/security checks passed against TOON and the installed Reactor formatter/runner. The full audit retains one preexisting moderate minimatch advisory in development lint tooling. Audit results cover known reported advisories, not a comprehensive security assessment.
MCP output policy Focused tests verify exact credential echoes and inputs, including JSON escaping and metadata; ordinary pagination tokens and unrelated provider fields are preserved. Provider names and arbitrary secret patterns are intentionally not classified.
Repository checks pnpm check: 518 passed, 18 opt-in tests skipped; typecheck, lint, and production build passed in an isolated directory with PostgreSQL. CI and both Socket checks passed. Skipped opt-in tests are not counted as completed live verification.

The repeatable production demo is documented in provider tests. Authenticated model conversations and live iMessage delivery remain separate verification steps.

Code flow

Before this PR, all channels reach the existing Eve agent and its browser/integration tools:

flowchart LR
    Message["Web/API or iMessage via Linq"] --> Eve["Eve conversation and model"]
    Eve --> Existing["Existing browser and integration tools"]
    Existing --> Results["Tool results"]
    Results --> Eve
    Eve --> Reply["Reply in the same conversation"]
Loading

The PR adds a Poppy tool path alongside those existing tools. Green nodes are new Bell code; blue nodes are published packages.

flowchart TD
    Message["Existing message entrypoints"] --> Eve["Same Eve conversation and model"]
    Eve --> Existing["Existing browser and integration tools"]
    Existing --> Eve
    Eve --> Tools["New Poppy tools: owner identity and account approvals"]
    Tools --> App["client.ts: account orchestration"]
    App <--> Store["store.ts: encrypted owner/company state"]
    App --> SDK["Poppy SDK: discovery, signing and account authorization"]
    SDK --> Catalog["Official MCP client: fetch tool catalog"]
    Catalog --> Policy["apis.ts: provider read-only hints and known-credential input check"]
    Policy --> MCP["Official MCP client: schema validation and tool call"]
    MCP --> Provider["Company API"]
    Provider --> Output["apis.ts: filter known credential echoes and limit output"]
    Output --> Eve
    Eve --> Reply["Reply in the same conversation"]
    classDef added fill:#e6f4ea,stroke:#137333,color:#123
    classDef package fill:#e8f0fe,stroke:#1967d2,color:#123
    class Tools,App,Store,Policy,Output added
    class SDK,Catalog,MCP package
Loading

Account connection additionally pauses for Bell approval and the provider’s human consent flow. Public agent metadata/JWKS routes let providers verify Bell’s signing identity. The diagram shows logical control/data flow; the SDKs own their HTTP requests and responses.

@shrey150
shrey150 requested a review from a team as a code owner November 13, 2026 18:14
Comment thread server/poppy/client.ts Outdated
let service = api.url;
const advertised = new Set([...Object.keys(company.scopes), ...(company.oauth?.scopes_supported ?? [])]);
if (api.type === "openapi") {
const description = await this.describe(owner, conversation, url, apiIndex, signal);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we able to leveragedescribePoppyApi() directly here so reconnecting does not depend on the existing grant? after a failing refresh, the saved grant essentially becomes uncertain. this call to describe() reaches accessToken(), which rejects that grant before device authorization can start. the user is told to reconnect, however reconnecting will j produce the same error.

i'd recommend reproing this w/ a mocked refresh failure followed by client.connect(). also worth noting - pls fetch the API metadata w/o using the existing grant, and you'll prob want to add a regression test confirming that a failed token refresh does not prevent the user from starting sign in again.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old path was a real reconnect trap. The SDK refactor removed connect() → authenticated describe()/accessToken(): connection now validates public discovery metadata and calls DeviceSignInClient.begin() directly.

Added a regression in d4fd356 for failed refresh → explicit disconnect → fresh device sign-in. It passes with the old refresh endpoint still failing and asserts that fresh sign-in performs no MCP call or refresh-token exchange. Bell intentionally continues to require disconnect before replacing a saved or uncertain grant, so recovery does not silently discard access that has not been revoked.

Comment thread server/poppy/store.ts Outdated
async withCompany<T>(owner: string, company: string, operation: (state: PoppyCompanyState, save: () => Promise<void>) => Promise<T>): Promise<T> {
if (!z.string().min(1).max(2048).safeParse(owner).success || !httpsUrl.safeParse(company).success)
throw new Error("Poppy state requires an authenticated owner and company identifier.");
const work = (pending.get(this.#path) ?? Promise.resolve()).then(async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we should scope the network-spanning lock to the owner/company, and keep shared file updates in a separate, shorter section?

this queue is keyed only by the file path and stays locked throughout operation(), including remote API and oauth requests. therefore, a stalled provider therefore blocks unrelated users' reads, status checks, and disconnects.

u can double check this by holding one user's read open: another user's API inspection wont be able to finish until that read was released. you'll also prob want to ensure that serialization is preserved for the same owner/company, and keep the shared file read/merge/write atomic to prevent lost updates.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d4fd356. Reproduced the file-wide blockage with a held provider operation; the regression failed before the fix and now passes.

Network work is serialized by file + owner + company using the SDK's public InProcessRefreshTokenLock. A separate, short file lock rereads current encrypted state, merges only the checkpointed record, and saves atomically. Tests verify that another owner and another company complete while the first request is held, the same account stays serialized across store instances, concurrent updates survive, and corrupt shared state is not overwritten. The store remains single-process.

@socket-security

socket-security Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​datalayer/​personal-agent-protocol@​0.35.06910010097100
Added@​modelcontextprotocol/​client@​2.4.0921008295100
Added@​datalayer/​reactor@​1.3.7871009796100
Addedjose@​6.2.1710010010095100

View full report

@shrey150 shrey150 changed the title Add Poppy API discovery and authenticated reads Add Poppy discovery and read-only MCP tools using SDKs Nov 13, 2026
@shrey150
shrey150 marked this pull request as draft November 13, 2026 18:41
@shrey150
shrey150 marked this pull request as ready for review November 13, 2026 21:26

@shriyatheunicorn shriyatheunicorn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few comments abt failure handling, other lgtm! lmk when rereview and ill stamp

Comment thread server/poppy/client.ts
await save();
if (!token) return { error: "auth_required", message: "Reconnect this account before reading." };
}
const auth = new McpSessionAuth(api.url, { ...runtime, session, token });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once a provider ends a session, the subsequent reads keep using the rejected session ID. McpSessionAuth signs the requests, but it doesnt start a replacement session, so the user will j keep getting a generic API error. maybe we can use the Poppy SDK's recovery handling to retry once (w a new session ID) and save it for future reads? this was reproducible across two consecutive reads after a session_ended response

Comment thread server/poppy/client.ts
let token;
if (state.grant) {
// A process/storage failure during credential mutation must not replay an old grant.
state.blocked = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okie here, a temporary metadata outage currently forces the user to disconnect and sign in again. basically,blocked is saved before tokenFor() fetches the API's resource metadata. if that fetch returns a 503, the account remains blocked even though no token exchange happened. i'd fetch the metadata before setting this checkpoint, while keeping the checkpoint before the actual token exchange

Comment thread server/poppy/sdk.ts
}
const body = request.body ? await beforeAbort(request.arrayBuffer(), deadline) : undefined;
if (body && body.byteLength > MAX_REQUEST_BYTES) throw new Error("Poppy request exceeds Bell's size limit.");
const response = await beforeAbort(pinnedFetch(request.url, {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the rate limit regression here returned 429 with Retry-After: 60, but the MCP client immediately sent another request to the same endpoint: server/discover was followed by initialize. the fallback isn't prevented by maxRetries: 0. so we prob need to enforce the provider's wait time in the shared request path (including across separate tool calls) and return the retry timing instead of a generic API error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants