mcpField
S
SendLetter

SendLetter

远程服务
sendletter.eu通信协作07 周前更新

关于 SendLetter

SendLetter 是面向 Node 的服务和库,可准备并邮寄信件至其支持的欧洲目的地。它提供地址验证、报价、状态追踪,并通过幂等键和测试模式控制发送流程。

主要功能

  1. 01邮寄打印后的实体信件
  2. 02验证地址并获取报价
  3. 03追踪状态并下载信件 PDF
  4. 04在允许时取消信件
  5. 05获取发票和贷项通知单
  6. 06无费用测试发送流程

适用场景

  1. 01邮寄发票或提醒信
  2. 02发送前检查地址和邮资
  3. 03追踪邮寄并下载寄送凭证
  4. 04正式发送前测试信件工作流

MCP 相关

输入介绍了 SendLetter 库和 API,但未说明其 MCP 功能。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://sendletter.eu/api/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

详细介绍

sendletter

从 Node 寄出一封真正的信件——打印、折叠、贴上邮票,然后交给邮局。

npm install sendletter
import { SendLetter } from 'sendletter'

const sendletter = new SendLetter(process.env.SENDLETTER_API_KEY!)

const input = {
  idempotencyKey: 'invoice-2026-0042-reminder-1',
  sender: {
    company: 'Twilper',
    name: 'Gert Snijder',
    street: 'Merelstraat',
    number: '64',
    postalCode: '8916 AX',
    city: 'Leeuwarden',
    country: 'NL',
  },
  recipient: {
    name: 'Jan de Vries',
    street: 'Keizersgracht',
    number: '123',
    postalCode: '1015 CJ',
    city: 'Amsterdam',
    country: 'NL',
  },
  text: 'Beste heer De Vries,\n\nBijgaand de herinnering voor factuur 2026-0042.',
}

const prepared = await sendletter.prepare(input)
console.log(prepared.id, prepared.pages, prepared.totalCents)
const pdf = await sendletter.download(prepared.id)

向用户展示 PDF 和确切的总金额。获得批准后,使用相同的输入和密钥:

const letter = await sendletter.send({ ...input, expectedTotalCents: prepared.totalCents })

console.log(letter.id, letter.status, letter.totalCents)

在 sendletter.eu 获取密钥。以 sk_test_ 开头的密钥不会寄出任何信件,也不会产生费用,但仍会运行完整的状态链,因此你可以基于真实服务进行开发。

寄信

只通过一种方式提供内容:text、document(富文本)、file (base64)或 fileUrl。如果提供两种,系统会拒绝请求而不是猜测,因为投进邮筒的错误文件无法撤回。

// An invoice you already have as a PDF.
await sendletter.send({
  sender,
  recipient,
  file: { name: 'invoice.pdf', contentBase64: pdf.toString('base64') },
  product: 'registered',        // 'standard' | 'priority' | 'registered'
  colour: true,
  idempotencyKey: `invoice-${invoice.id}`,
})

对于任何可能重试的请求,都请发送 idempotencyKey。 使用相同的密钥重复请求时,系统会返回原信件,而不会再寄出第二封信。这个字段能确保网络超时后重试是安全的;超时并不能说明信件是否已被接受。

查询

await sendletter.get(id)                       // one letter
await sendletter.list({ status: 'posted' })    // one page
await sendletter.cancel(id, 'order withdrawn') // while it is still cancellable
await sendletter.download(id)                  // the PDF as printed
await sendletter.download(id, { proof: true }) // proof of posting

for await (const letter of sendletter.all({ mode: 'live' })) {
  // walks every page; you never hold the cursor
}

发票和贷项通知单

每封已付款的正式信件都会在付款时获得发票编号。测试信件不会占用法定编号序列。全额退款会保留该发票,并另外生成一张单独编号的负数贷项通知单。

const page = await sendletter.listInvoices()
await writeFile('invoice.pdf', await sendletter.downloadInvoice(page.data[0].id))

if (page.data[0].creditNote) {
  await writeFile(
    'credit-note.pdf',
    await sendletter.downloadCreditNote(page.data[0].creditNote.id),
  )
}

发送前检查

const check = await sendletter.validateAddress(recipient)
if (!check.valid) console.log(check.problems)   // answers, does not throw

const quote = await sendletter.quote({ destination: 'DE', pages: 3 })

validateAddress 会返回错误地址,而不是抛出异常:询问地址是否错误,得到错误地址就是成功结果。supported: false 则表示无法通过修改地址来解决——我们不向该国家或地区邮寄。

错误

API 拒绝的所有请求都会转为带有 code 的 SendLetterError,供你据此进行分支处理。

import { SendLetterError } from 'sendletter'

try {
  await sendletter.send({ sender, recipient, text })
} catch (error) {
  if (!(error instanceof SendLetterError)) throw error

  if (error.code === 'insufficient_balance') {
    // Put this in front of the customer; the wallet is short, nothing else.
    return redirect(error.topUpUrl!)
  }
  if (error.isRetryable) {
    await sleep((error.retryAfter ?? 5) * 1000)
  }
}

isRetryable 涵盖 429 和 5xx。其他情况都不应重试:如果是 400,第二次请求也同样会被拒绝。

Webhook

状态变更会以 POST 请求的方式发送到你注册的网址。请验证这些请求。 否则,任何知道你的端点的人都能告诉你的系统某封信已送达。

import { verifyWebhook, SendLetterError } from 'sendletter'

export async function POST(request: Request) {
  const rawBody = await request.text()   // the raw text, not the parsed object

  let event
  try {
    event = verifyWebhook({
      rawBody,
      signature: request.headers.get('x-sendletter-signature'),
      secret: process.env.SENDLETTER_WEBHOOK_SECRET!,
    })
  } catch (error) {
    if (error instanceof SendLetterError) return new Response('nope', { status: 400 })
    throw error
  }

  if (await alreadyHandled(event.id)) return new Response('ok')   // see below
  await handle(event)                                            // letter.posted, ...
  return new Response('ok')
}

这种形式做对了两件手动编写验证代码时通常会忽略的事:

  • 验证原始请求体,而不是重新序列化后的内容。 签名覆盖的是我们发送的确切字节,而 JSON.stringify 不保证能还原这些字节。先以文本形式读取请求体、完成验证,然后再解析。
  • 根据 event.id 去重。 系统采用至少一次投递并会重试,因此如果你的服务发生超时,同一事件就会再次到达。对 letter.posted 事件的重复处理不应导致向客户重复收费。

事件:letter.submitted、letter.printed、letter.posted、 letter.delivered、letter.failed、letter.refunded。

测试模式

sk_test_ 密钥会运行完整链路——submitted → printed → posted → delivered——并触发与生产环境完全相同的 Webhook,但不会动用钱包或打印机。client.isTestMode 会告诉你当前使用的是哪种密钥;最好在部署检查中断言这一点:它们在日志中看起来一模一样,但只有一种会产生费用。

参考

sendletter.eu/en/developers · OpenAPI 位于 /api/v1/openapi.json · MIT

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。