From c0e0497ce9222908c850ea1c4aab326d9cb3322c Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 15:47:54 +0300 Subject: [PATCH 1/7] feat: show error history and stop calling failed and unknown final Co-Authored-By: Claude Opus 5.5 --- src/format.ts | 20 ++++++++++++++++---- src/tools.ts | 2 +- test/format.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/format.ts b/src/format.ts index 240c2fc..0d3f11b 100644 --- a/src/format.ts +++ b/src/format.ts @@ -2,8 +2,11 @@ import type { Device, Message, MessageList } from '@textbee/sdk' const BODY_PREVIEW_CHARS = 300 -/** Statuses that will not change again; anything else may still progress. */ -export const FINAL_STATUSES = ['delivered', 'failed', 'unknown'] as const +/** Statuses that will not change again. failed and unknown can still change. */ +export const FINAL_STATUSES = ['delivered'] as const + +// Local shape until the published SDK types carry errorHistory. +type WithErrorHistory = { errorHistory?: unknown[] } /** * Direction of a message. Prefer the lowercase `direction` field; fall back to @@ -41,7 +44,16 @@ function formatMessageLine(m: Message, index: number): string { : `to ${m.recipient ?? 'unknown number'}` const status = m.status ?? 'unknown' const header = `[${index}] ${when} ${direction} ${party} status=${status} id=${m._id}` - return `${header}\n ${messageBody(m)}` + const lines = [header, ` ${messageBody(m)}`] + if (status === 'failed' && (m.errorCode || m.errorMessage)) { + lines.push(` error: ${[m.errorCode, m.errorMessage].filter(Boolean).join(' - ')}`) + } else if (status === 'sent' || status === 'delivered') { + const failures = (m as WithErrorHistory).errorHistory?.length ?? 0 + if (failures > 0) { + lines.push(` failed ${failures} time${failures === 1 ? '' : 's'} before it was ${status}`) + } + } + return lines.join('\n') } export interface FormatMessagesContext { @@ -71,7 +83,7 @@ export function formatMessages(list: MessageList, ctx: FormatMessagesContext): s lines.push('') lines.push( - `Statuses ${FINAL_STATUSES.join(', ')} are final; pending, dispatched, and sent may still change, so wait before re-checking.`, + 'Status delivered is final. failed and unknown can still change (a late phone report or a recovery), and pending, dispatched, and sent may still change, so re-check later.', ) if (meta.nextCursor) { diff --git a/src/tools.ts b/src/tools.ts index 5e5f245..750d277 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -262,7 +262,7 @@ export function buildToolDefinitions(opts: BuildOptions): TextbeeToolDefinition[ lines.push(`sms_batch_id: ${result.smsBatchId}`) lines.push( `Check per-recipient delivery with get_messages sms_batch_id="${result.smsBatchId}". ` + - 'Statuses delivered, failed, and unknown are final.', + 'Status delivered is final. failed and unknown can still change.', ) } else { const immediate = result as { successCount?: number; failureCount?: number } diff --git a/test/format.test.ts b/test/format.test.ts index 08f8e9d..fe38e59 100644 --- a/test/format.test.ts +++ b/test/format.test.ts @@ -48,6 +48,30 @@ describe('formatMessages', () => { expect(text).toContain('66b0000000000000000001aa') }) + it('shows the current error on a failed message', () => { + const list: MessageList = { + data: [message({ direction: 'sent', status: 'failed', errorCode: 'FCM_SEND_FAILED', errorMessage: 'push failed' })], + meta: baseMeta, + } + const text = formatMessages(list, { direction: 'all', usedCursor: false, order: 'desc' }) + expect(text).toContain('error: FCM_SEND_FAILED - push failed') + }) + + it('notes earlier failures on a message that later succeeded', () => { + const entry = { code: 'FCM_SEND_FAILED', at: '2026-10-01T10:00:00.000Z', source: 'push' } + const list: MessageList = { + data: [message({ direction: 'sent', status: 'sent', errorHistory: [entry, entry] } as Partial)], + meta: baseMeta, + } + const text = formatMessages(list, { direction: 'all', usedCursor: false, order: 'desc' }) + expect(text).toContain('failed 2 times before it was sent') + }) + + it('says failed and unknown can still change', () => { + const text = formatMessages({ data: [], meta: baseMeta }, { direction: 'all', usedCursor: false, order: 'desc' }) + expect(text).toContain('failed and unknown can still change') + }) + it('renders missing status and missing body as unknown, never undefined', () => { const list: MessageList = { data: [message({ message: undefined, status: undefined } as unknown as Partial)], From 34803e2c1d2628d3cfe7723c1c19840da8c81d77 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 16:15:51 +0300 Subject: [PATCH 2/7] fix: soften final-status wording, neutral failure count, clearer error line Co-Authored-By: Claude Opus 5.5 --- src/format.ts | 23 ++++++++++++++--------- src/tools.ts | 2 +- test/format.test.ts | 38 ++++++++++++++++++++++++++++++++++---- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/format.ts b/src/format.ts index 0d3f11b..74d61c0 100644 --- a/src/format.ts +++ b/src/format.ts @@ -2,7 +2,7 @@ import type { Device, Message, MessageList } from '@textbee/sdk' const BODY_PREVIEW_CHARS = 300 -/** Statuses that will not change again. failed and unknown can still change. */ +/** Statuses that rarely change. failed and unknown can still change. */ export const FINAL_STATUSES = ['delivered'] as const // Local shape until the published SDK types carry errorHistory. @@ -45,13 +45,18 @@ function formatMessageLine(m: Message, index: number): string { const status = m.status ?? 'unknown' const header = `[${index}] ${when} ${direction} ${party} status=${status} id=${m._id}` const lines = [header, ` ${messageBody(m)}`] - if (status === 'failed' && (m.errorCode || m.errorMessage)) { - lines.push(` error: ${[m.errorCode, m.errorMessage].filter(Boolean).join(' - ')}`) - } else if (status === 'sent' || status === 'delivered') { - const failures = (m as WithErrorHistory).errorHistory?.length ?? 0 - if (failures > 0) { - lines.push(` failed ${failures} time${failures === 1 ? '' : 's'} before it was ${status}`) - } + const state = String(m.status ?? '').toLowerCase() + if (state === 'failed' && (m.errorCode || m.errorMessage)) { + const code = m.errorCode ?? '' + const text = m.errorMessage ?? '' + const detail = code && text && !text.startsWith(code) ? `${code} - ${text}` : text || code + lines.push(` error: ${detail}`) + } else if (state === 'unknown') { + lines.push(' no report from the phone within 20 minutes (not a failure)') + } + const failures = (m as WithErrorHistory).errorHistory?.length ?? 0 + if (failures > 0) { + lines.push(` ${failures >= 10 ? '10 or more' : failures} failure${failures === 1 ? '' : 's'} recorded`) } return lines.join('\n') } @@ -83,7 +88,7 @@ export function formatMessages(list: MessageList, ctx: FormatMessagesContext): s lines.push('') lines.push( - 'Status delivered is final. failed and unknown can still change (a late phone report or a recovery), and pending, dispatched, and sent may still change, so re-check later.', + 'Status delivered rarely changes. failed and unknown can still change (a late phone report or a recovery). pending, dispatched, and sent are in progress. The user can ask to check again later.', ) if (meta.nextCursor) { diff --git a/src/tools.ts b/src/tools.ts index 750d277..bd70844 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -262,7 +262,7 @@ export function buildToolDefinitions(opts: BuildOptions): TextbeeToolDefinition[ lines.push(`sms_batch_id: ${result.smsBatchId}`) lines.push( `Check per-recipient delivery with get_messages sms_batch_id="${result.smsBatchId}". ` + - 'Status delivered is final. failed and unknown can still change.', + 'Status delivered rarely changes. failed and unknown can still change.', ) } else { const immediate = result as { successCount?: number; failureCount?: number } diff --git a/test/format.test.ts b/test/format.test.ts index fe38e59..e665fec 100644 --- a/test/format.test.ts +++ b/test/format.test.ts @@ -57,19 +57,49 @@ describe('formatMessages', () => { expect(text).toContain('error: FCM_SEND_FAILED - push failed') }) - it('notes earlier failures on a message that later succeeded', () => { + it('counts recorded failures neutrally', () => { const entry = { code: 'FCM_SEND_FAILED', at: '2026-10-01T10:00:00.000Z', source: 'push' } + const fmt = (n: number) => + formatMessages( + { data: [message({ direction: 'sent', status: 'sent', errorHistory: Array(n).fill(entry) } as Partial)], meta: baseMeta }, + { direction: 'all', usedCursor: false, order: 'desc' }, + ) + expect(fmt(1)).toContain('1 failure recorded') + expect(fmt(3)).toContain('3 failures recorded') + expect(fmt(10)).toContain('10 or more failures recorded') + expect(fmt(1)).not.toContain('before it was') + }) + + it('compares status case-insensitively', () => { const list: MessageList = { - data: [message({ direction: 'sent', status: 'sent', errorHistory: [entry, entry] } as Partial)], + data: [message({ direction: 'sent', status: 'FAILED' as never, errorCode: 'X_CODE', errorMessage: 'broke' })], meta: baseMeta, } const text = formatMessages(list, { direction: 'all', usedCursor: false, order: 'desc' }) - expect(text).toContain('failed 2 times before it was sent') + expect(text).toContain('error: X_CODE - broke') }) - it('says failed and unknown can still change', () => { + it('prints only the message when it already starts with the code', () => { + const list: MessageList = { + data: [message({ direction: 'sent', status: 'failed', errorCode: 'FCM_SEND_TIMEOUT', errorMessage: 'FCM_SEND_TIMEOUT: no ack' })], + meta: baseMeta, + } + const text = formatMessages(list, { direction: 'all', usedCursor: false, order: 'desc' }) + expect(text).toContain('error: FCM_SEND_TIMEOUT: no ack') + expect(text).not.toContain('FCM_SEND_TIMEOUT - ') + }) + + it('explains an unknown status is not a failure', () => { + const list: MessageList = { data: [message({ direction: 'sent', status: 'unknown' })], meta: baseMeta } + const text = formatMessages(list, { direction: 'all', usedCursor: false, order: 'desc' }) + expect(text).toContain('no report from the phone within 20 minutes (not a failure)') + }) + + it('says failed and unknown can still change and delivered rarely changes', () => { const text = formatMessages({ data: [], meta: baseMeta }, { direction: 'all', usedCursor: false, order: 'desc' }) expect(text).toContain('failed and unknown can still change') + expect(text).toContain('delivered rarely changes') + expect(text).not.toContain('is final') }) it('renders missing status and missing body as unknown, never undefined', () => { From c0aa63548ead49017ea7cdff0b2c1df649432676 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 16:24:49 +0300 Subject: [PATCH 3/7] fix: only skip the code when the message starts with it as a prefix Co-Authored-By: Claude Opus 5.5 --- src/format.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/format.ts b/src/format.ts index 74d61c0..d5092ca 100644 --- a/src/format.ts +++ b/src/format.ts @@ -49,7 +49,7 @@ function formatMessageLine(m: Message, index: number): string { if (state === 'failed' && (m.errorCode || m.errorMessage)) { const code = m.errorCode ?? '' const text = m.errorMessage ?? '' - const detail = code && text && !text.startsWith(code) ? `${code} - ${text}` : text || code + const detail = code && text && !text.startsWith(`${code}:`) ? `${code} - ${text}` : text || code lines.push(` error: ${detail}`) } else if (state === 'unknown') { lines.push(' no report from the phone within 20 minutes (not a failure)') From 62c94c1ef9f32f82e59eb265e82ef3525337cfee Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 16:32:55 +0300 Subject: [PATCH 4/7] fix: cap failure count wording at 15 Co-Authored-By: Claude Opus 5.5 --- src/format.ts | 2 +- test/format.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/format.ts b/src/format.ts index d5092ca..c91d753 100644 --- a/src/format.ts +++ b/src/format.ts @@ -56,7 +56,7 @@ function formatMessageLine(m: Message, index: number): string { } const failures = (m as WithErrorHistory).errorHistory?.length ?? 0 if (failures > 0) { - lines.push(` ${failures >= 10 ? '10 or more' : failures} failure${failures === 1 ? '' : 's'} recorded`) + lines.push(` ${failures >= 15 ? '15 or more' : failures} failure${failures === 1 ? '' : 's'} recorded`) } return lines.join('\n') } diff --git a/test/format.test.ts b/test/format.test.ts index e665fec..61d4876 100644 --- a/test/format.test.ts +++ b/test/format.test.ts @@ -66,7 +66,8 @@ describe('formatMessages', () => { ) expect(fmt(1)).toContain('1 failure recorded') expect(fmt(3)).toContain('3 failures recorded') - expect(fmt(10)).toContain('10 or more failures recorded') + expect(fmt(10)).toContain('10 failures recorded') + expect(fmt(15)).toContain('15 or more failures recorded') expect(fmt(1)).not.toContain('before it was') }) From 0b16962ee37b777b459eccfb5baf17623ad91df7 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 16:48:23 +0300 Subject: [PATCH 5/7] fix: do not count delivery report entries as failures Co-Authored-By: Claude Opus 5.5 --- src/format.ts | 8 ++++++-- test/format.test.ts | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/format.ts b/src/format.ts index c91d753..51c15a3 100644 --- a/src/format.ts +++ b/src/format.ts @@ -6,7 +6,7 @@ const BODY_PREVIEW_CHARS = 300 export const FINAL_STATUSES = ['delivered'] as const // Local shape until the published SDK types carry errorHistory. -type WithErrorHistory = { errorHistory?: unknown[] } +type WithErrorHistory = { errorHistory?: Array<{ deliveryReport?: boolean }> } /** * Direction of a message. Prefer the lowercase `direction` field; fall back to @@ -54,10 +54,14 @@ function formatMessageLine(m: Message, index: number): string { } else if (state === 'unknown') { lines.push(' no report from the phone within 20 minutes (not a failure)') } - const failures = (m as WithErrorHistory).errorHistory?.length ?? 0 + const history = (m as WithErrorHistory).errorHistory ?? [] + const failures = history.filter((e) => !e?.deliveryReport).length if (failures > 0) { lines.push(` ${failures >= 15 ? '15 or more' : failures} failure${failures === 1 ? '' : 's'} recorded`) } + if (history.length > failures) { + lines.push(' delivery report not confirmed (the message was sent)') + } return lines.join('\n') } diff --git a/test/format.test.ts b/test/format.test.ts index 61d4876..5a0dd17 100644 --- a/test/format.test.ts +++ b/test/format.test.ts @@ -71,6 +71,23 @@ describe('formatMessages', () => { expect(fmt(1)).not.toContain('before it was') }) + it('does not count delivery report entries as failures', () => { + const fail = { code: 'FCM_SEND_FAILED', at: '2026-10-01T10:00:00.000Z', source: 'push' } + const report = { code: 'DELIVERY_FAILED', at: '2026-10-01T10:05:00.000Z', source: 'device', deliveryReport: true } + const fmt = (history: unknown[]) => + formatMessages( + { data: [message({ direction: 'sent', status: 'sent', errorHistory: history } as Partial)], meta: baseMeta }, + { direction: 'all', usedCursor: false, order: 'desc' }, + ) + const only = fmt([report]) + expect(only).toContain('delivery report not confirmed (the message was sent)') + expect(only).not.toContain('failure') + const mixed = fmt([fail, report]) + expect(mixed).toContain('1 failure recorded') + expect(mixed).toContain('delivery report not confirmed (the message was sent)') + expect(fmt([fail])).not.toContain('delivery report') + }) + it('compares status case-insensitively', () => { const list: MessageList = { data: [message({ direction: 'sent', status: 'FAILED' as never, errorCode: 'X_CODE', errorMessage: 'broke' })], From 62533934d34dd8071e29f22849bb70e20e1c9568 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 17:53:47 +0300 Subject: [PATCH 6/7] refactor: read issueHistory instead of errorHistory Co-Authored-By: Claude Opus 5.5 --- src/format.ts | 6 +++--- test/format.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/format.ts b/src/format.ts index 51c15a3..161ddd2 100644 --- a/src/format.ts +++ b/src/format.ts @@ -5,8 +5,8 @@ const BODY_PREVIEW_CHARS = 300 /** Statuses that rarely change. failed and unknown can still change. */ export const FINAL_STATUSES = ['delivered'] as const -// Local shape until the published SDK types carry errorHistory. -type WithErrorHistory = { errorHistory?: Array<{ deliveryReport?: boolean }> } +// Local shape until the published SDK types carry issueHistory. +type WithIssueHistory = { issueHistory?: Array<{ deliveryReport?: boolean }> } /** * Direction of a message. Prefer the lowercase `direction` field; fall back to @@ -54,7 +54,7 @@ function formatMessageLine(m: Message, index: number): string { } else if (state === 'unknown') { lines.push(' no report from the phone within 20 minutes (not a failure)') } - const history = (m as WithErrorHistory).errorHistory ?? [] + const history = (m as WithIssueHistory).issueHistory ?? [] const failures = history.filter((e) => !e?.deliveryReport).length if (failures > 0) { lines.push(` ${failures >= 15 ? '15 or more' : failures} failure${failures === 1 ? '' : 's'} recorded`) diff --git a/test/format.test.ts b/test/format.test.ts index 5a0dd17..33b51b3 100644 --- a/test/format.test.ts +++ b/test/format.test.ts @@ -61,7 +61,7 @@ describe('formatMessages', () => { const entry = { code: 'FCM_SEND_FAILED', at: '2026-10-01T10:00:00.000Z', source: 'push' } const fmt = (n: number) => formatMessages( - { data: [message({ direction: 'sent', status: 'sent', errorHistory: Array(n).fill(entry) } as Partial)], meta: baseMeta }, + { data: [message({ direction: 'sent', status: 'sent', issueHistory: Array(n).fill(entry) } as Partial)], meta: baseMeta }, { direction: 'all', usedCursor: false, order: 'desc' }, ) expect(fmt(1)).toContain('1 failure recorded') @@ -76,7 +76,7 @@ describe('formatMessages', () => { const report = { code: 'DELIVERY_FAILED', at: '2026-10-01T10:05:00.000Z', source: 'device', deliveryReport: true } const fmt = (history: unknown[]) => formatMessages( - { data: [message({ direction: 'sent', status: 'sent', errorHistory: history } as Partial)], meta: baseMeta }, + { data: [message({ direction: 'sent', status: 'sent', issueHistory: history } as Partial)], meta: baseMeta }, { direction: 'all', usedCursor: false, order: 'desc' }, ) const only = fmt([report]) From a7b17b7a4a3ddd74e56897b3c991499833bc2802 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 18:17:42 +0300 Subject: [PATCH 7/7] chore: release 0.1.0 Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- server.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2f06a5d..da971f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@textbee/mcp", - "version": "0.0.2", + "version": "0.1.0", "mcpName": "io.github.textbee/mcp", "description": "MCP server for textbee.dev. Give your AI agent a phone number: send and read SMS through your own Android phone.", "license": "MIT", diff --git a/server.json b/server.json index 6134c28..8437216 100644 --- a/server.json +++ b/server.json @@ -8,13 +8,13 @@ "source": "github" }, "websiteUrl": "https://textbee.dev/mcp", - "version": "0.0.2", + "version": "0.1.0", "packages": [ { "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "@textbee/mcp", - "version": "0.0.2", + "version": "0.1.0", "transport": { "type": "stdio" },