Skip to main content
Glama
Weone404

weone-daily-post

by Weone404

weone-daily-post — 远程 MCP 服务器

We One Aviation 每日 Instagram + Facebook 帖子的发布后端。 它是一个无状态的 Streamable HTTP MCP 服务器: Claude 负责思考(主题选择、文案、标题),此服务负责副作用(历史记录、图像渲染、存储、Graph API)。 图像是排版生成的,而非 AI 生成的:海报上的文字与提供的文字完全一致。

Claude ──POST /mcp (Bearer)──▶ Render web service (Node 20, Express)
                                 ├─ Supabase  posts table + post-images bucket
                                 ├─ Chromium      HTML template → JPEG
                                 └─ Meta Graph  IG container/publish, FB photos

工具

工具

用途

get_past_topics()

最新优先的历史记录,最多 200 行:{id, topic, category, status, created_at}。选择主题前请先读取。

reserve_topic(topic, category)

插入 status='reserved',返回 {id}。category ∈ news、subject、career。重复主题会失败并返回 duplicate_topic。

render_post({template, headline, points, footer?, eyebrow?, slug?})

将品牌化 HTML 模板渲染为精确尺寸的 sRGB JPEG,上传,并返回一个图像块以及公共 URL。

publish_socials(image_url, caption, hashtags, history_id)

先发布到 Instagram,再发布到 Facebook,并记录结果。

mark_draft(history_id, image_url, caption)

影子模式:将完成的帖子记录为 draft,不进行发布。

check_token()

Meta 令牌到期前的天数 + 已授予的权限范围。

每个工具都返回 JSON。成功时返回 {"ok": true, ...};失败时返回 MCP 错误 结果,包含 {"ok": false, "error": {code, message, retryable, details}}。 不会向调用方抛出原始堆栈跟踪。

render_post

template 是 news、subject 或 career 之一 — 与 posts 表使用的三个类别相同。

字段

限制

说明

headline

60 个字符

Barlow 700,最多 3 行。句子大小写,非标题大小写。

points

3–4 项,每项 90 个字符

Barlow 400,每项一个金色标记

eyebrow

32 个字符,可选

金色,由 CSS 转为大写,例如 NAVIGATION

footer

90 个字符,可选

页脚栏左侧,例如 DGCA · 14 Aug 2026

它返回两个内容块:一个图像块(base64 JPEG)和一个文本 块,包含公共 URL、文件名、尺寸、字节大小和渲染时间。

在上传任何内容之前会运行两个保护检查,两者都会指明违规字段:

  1. 长度限制,在触及 Chromium 之前检查 — 这是廉价的拒绝方式。 points[2] is 97 characters, limit is 90. Shorten it and retry.

  2. 页面内测量,在布局之后 — 每个文本框都是一个固定大小的 裁剪框,如果其内容高于或宽于该框,渲染将被拒绝,并返回字段名称和溢出的像素数。这能捕获字符计数无法发现的问题, 例如一个长度合法但超出边缘的不可断开的 80 字符标记。

任一保护检查触发时都不会上传任何内容,因此拒绝只需一秒钟, 修复方法始终是"缩短指定字段"。

图像块仍然会返回,以便您能在上下文中阅读文字,但它不再是正确性检查: 固定模板不会拼错单词或虚构图表。最坏情况的合法输入(58 字符标题加四条 90 字符要点、最宽的合法眉题和页脚)已验证可适配所有三个模板。

publish_socials,逐步说明

  1. 对 image_url 执行 HEAD 请求并断言 200 + content-type: image/jpeg。Meta 会在服务器端获取此 URL,而错误的 URL 会在数小时后以不透明的方式失败。 (拒绝 HEAD 的存储会改用单字节范围 GET。)

  2. Instagram — full_caption = caption + "\n\n" + hashtags.join(' '),上限 为 2200 个字符。只会从末尾丢弃标签;标题正文 永远不会被截断。如果正文单独超过 2200 个字符,调用会在发布任何内容之前以 caption_too_long 失败。 POST {IG_USER_ID}/media → 每秒轮询一次 GET {container}?fields=status_code,status,最多 60 秒 → 仅在 FINISHED 时发布。在 ERROR 时, status 字符串会原样返回,因为这是 Meta 解释其不满之处的唯一位置。

  3. Facebook — 使用 url 和 message 执行 POST {FB_PAGE_ID}/photos。无论 Instagram 结果如何都会尝试。

  4. 记录 — posts 行会获得 ig_post_id、fb_post_id、image_url 和 status = published(两者都成功)、partial(其中一个成功)或 failed(两者都失败)。

返回 {ig_post_id, fb_post_id, status, errors: [...]}。单平台失败 绝不会被吞没:它会出现在 errors[] 中,包含平台、失败的阶段以及 Meta 自己的 code / error_subcode / message。

Related MCP server: Meta Social MCP

环境变量

变量

必需

说明

MCP_AUTH_TOKEN

是

/mcp 的共享密钥。连接器必须发送 Authorization: Bearer <value>。如果未设置,服务器仍会启动并服务 /health,但会以 500 拒绝每个 /mcp 请求 — 它默认关闭,绝不开放。使用 node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" 生成一个。

SUPABASE_URL

是

https://<project-ref>.supabase.co。

SUPABASE_SERVICE_KEY

是

服务角色密钥。绕过 RLS — 仅限服务器端。切勿将其放入连接器配置中。

META_GRAPH_VERSION

否(默认 v23.0)

每次调用使用的 Graph API 版本。

IG_USER_ID

用于发布

Instagram 商业账户 ID(数字,非 @用户名)。

FB_PAGE_ID

用于发布

与该 Instagram 账户关联的 Facebook 主页 ID。

META_PAGE_ACCESS_TOKEN

用于发布

长期有效的主页访问令牌,具有 instagram_basic、instagram_content_publish、pages_show_list、pages_read_engagement、pages_manage_posts 权限。约 60 天后过期 — check_token() 会告诉您剩余时间。

PORT

否

Render 会设置此项。默认 10000。

MAX_INLINE_IMAGE_BYTES

否(默认 1400000)

超过此大小后,内联 base64 预览会被缩小。

CHROMIUM_EXECUTABLE_PATH

否

Chrome/Chromium 二进制的显式路径。覆盖各平台默认值。

CHROMIUM_SINGLE_PROCESS

否

设置为 1 以强制 --single-process。代价是浏览器无法复用 — 每次启动只渲染一次。参见 浏览器生命周期。

将 .env.example 复制为 .env 以进行本地运行。.env 已被 gitignore — 请保持这样。

设置

1. Supabase

在 SQL 编辑器中运行 migrations/001_init.sql(或执行 supabase db push)。它是 幂等的,并创建:

  • posts 表,包含检查约束和 topic 上的唯一索引 — 该索引就是重复保护,因此重复保留应该失败,

  • created_at desc 和 status 索引,

  • 在 posts 上启用 RLS,且无任何策略(只有服务密钥可以进入),

  • 公共 post-images 存储桶及其公共读取策略。 公共读取是必需的:Meta 自行获取 JPEG,无法提供凭据。

2. Meta

您需要一个与 Facebook 主页关联的 Instagram 商业或创作者账户, 以及一个具有上述权限范围的长期有效主页令牌。在首次运行前使用 check_token() 确认 — 令牌过期是早晨发布失败最常见的原因。

3. 部署到 Render

使用 render.yaml(Blueprint):

  1. 将此仓库推送到 GitHub。

  2. Render 仪表板 → 新建 → Blueprint → 选择仓库。它会读取 render.yaml:Node 20、npm ci && npm run build、npm start、 在 /health 上进行健康检查。

  3. Render 会提示输入每个 sync: false 变量。粘贴它们。

  4. 部署,然后检查日志中是否有 server.listening ... auth=configured。 auth=MISSING 表示 MCP_AUTH_TOKEN 未设置。

手动方式:

  1. 新建 → Web 服务 → 连接仓库。

  2. 运行时 Node,构建 npm ci && npm run build,启动 npm start。

  3. 健康检查路径 /health。

  4. 添加上表中的环境变量,外加 NODE_VERSION=20。

验证:

curl https://<your-service>.onrender.com/health
# {"status":"ok","server":{...},"tools":[...six...],"uptime_s":3}

使用 Starter 计划,不要用免费版。 Chromium 在 Node 之上大约需要 400 MB 常驻内存,而免费实例只有 512 MB——它会在渲染中途 OOM,失败表现为一个死掉的 worker,而不是有用的日志行。免费版还会在无活动后休眠,所以每天第一次工具调用还要额外付出 30–60 秒的冷启动。render.yaml 出于这两个原因设置了 starter。

构建时不需要下载浏览器:@sparticuz/chromium 自带二进制文件作为依赖,所以 npm ci && npm run build 就是整个构建。该构建步骤还会把 src/templates/ 复制到 dist/——tsc 只生成 .ts,没有这一步服务器能正常启动,但第一次渲染时会因缺少模板文件而失败。

4. 连接到 Claude

端点是:

https://<your-service>.onrender.com/mcp

带有头:

Authorization: Bearer <MCP_AUTH_TOKEN>

Claude Code / Cowork CLI:

claude mcp add --transport http weone-social \
  https://<your-service>.onrender.com/mcp \
  --header "Authorization: Bearer <MCP_AUTH_TOKEN>"

.mcp.json(项目级,不带 token 提交):

{
  "mcpServers": {
    "weone-social": {
      "type": "http",
      "url": "https://<your-service>.onrender.com/mcp",
      "headers": { "Authorization": "Bearer ${MCP_AUTH_TOKEN}" }
    }
  }
}

在 Claude 桌面/网页 自定义连接器 对话框中,粘贴相同的 /mcp URL,并将 bearer token 放入请求头字段。认证设计上仅通过请求头——token 永远不会作为查询参数接受,因为 URL 会出现在代理日志和浏览器历史中。

每日运行说明(品牌规则、禁止声明、类别轮换、图片规格、QA 清单)位于 weone-daily-post 技能中,不在此服务器中。本服务刻意不持有任何编辑策略。

本地开发

npm install
cp .env.example .env      # fill it in
npm run dev               # tsx watch, http://localhost:10000
npm run typecheck
npm run build && npm start

npm run smoke

npm run smoke                 # render all three templates, upload, print 3 URLs
npm run smoke -- --no-upload  # render locally only, no credentials needed

渲染每种模板各一张,将三张 JPEG 写入 ./out,上传它们,对每个公开 URL 执行 HEAD 检查并打印三个链接。然后证明两个守卫仍然触发。它不触碰任何 Meta 端点,因此对生产凭据是安全的。上传只需要 SUPABASE_URL 和 SUPABASE_SERVICE_KEY;--no-upload 则什么都不需要。

本地文件在上传前写入,所以即使 Supabase 失败,仍有东西可看。

渲染管线

无头 Chromium 通过 file:// 加载 src/templates/{template}.html,值被写入 DOM,然后对页面截图。相同的输入总是产生相同的像素。

  • 模板 位于 src/templates/。tokens.css 保存所有颜色;base.css 保存三者共享的骨架。模板文件与其兄弟文件的区别仅在于眉题处理和点标记(新闻:金色规则,主题:编号金色圆圈,职业:金色 V 形)。

  • 字体自托管 在 src/templates/fonts/(Barlow 400/600/700 用于所有内容,Cinzel 600 仅用于字标,拉丁子集,OFL)。渲染时不获取任何内容——网络调用会使输出不确定,并在 Render 上静默失败,回退到系统衬线字体。渲染器等待 document.fonts.ready,然后断言两个字体确实加载,而不是截图回退字体。

  • 用户文本永远不会拼接进标记。 值通过 textContent 和 createElement 进入,所以没有转义错误:标题中的 <script> 会以字面字符形式出现在海报上。

  • 视口 1080×1350,deviceScaleFactor: 2,所以截图是 2160×2700 并降采样——文字边缘保持清晰。

  • sharp:resize(1080, 1350, {fit:'cover'}) → toColorspace('srgb') → jpeg({quality: 90, chromaSubsampling: '4:4:4'}),剥离元数据。4:4:4 不是装饰——4:2:0 会弄脏彩色文字边缘,而这些海报就是文字。

  • 断言编码后的 JPEG 小于 8 MB,并且解码后的尺寸确实是所要求的。

  • 上传为 {yyyy-mm-dd}-{slug}-{6 hex}.jpg(UTC 日期)。每次渲染都有自己的键,永远不会被覆盖——upsert: false。重新渲染一个主题不能改变已经发布先前 URL 的帖子下的图片。cacheControl 为 60 秒也是出于同样原因:坏对象在一分钟内可纠正,而不是在 CDN 中固定一年。Meta 在上传后不久、服务器端获取一次 URL,所以不需要长缓存。对象会累积;存储远比帖子上的过时图片便宜。

布局行为

类型缩放到点数。 三个点得到 68px 标题和 36px 正文;四个点得到 60px 和 32px。这是用 CSS 的 :has() 完成的,所以布局决策完全在模板中,渲染器既不知道也不关心。溢出守卫在缩放后运行,所以测量的是缩放后的结果。

内容块在头部规则和底部栏之间垂直居中。 仅靠固定间隙无法保持填充目标,因为文本量变化,所以三个弹性元素共享剩余空间:上方带、下方带和标题下方的间隙。带硬上限为 150px,这强制执行“无大空白边距”;一旦达到上限,多余空间进入标题间隙,读起来像呼吸空间而不是空洞。

代表性内容上的测量垂直填充:74–79%,带 99–124px。一个刻意稀疏的情况(一行标题,三个一行点)位于 68.8%,带处于 150px 上限——对于这么少的文本,这是算术最大值,再提高意味着点分散得太远,不再像列表。

标题是 Barlow 700 句子大小写,行高 1.1,字距 −0.5px。Cinzel 仅存在于“WE ONE AVIATION”字标中。句子大小写不在代码中强制执行——机械地小写标题会破坏 DGCA、ATPL 和 AAI——所以它在 headline 字段描述中指定。

每个模板内联一个扁平 SVG 装饰:宽对角线规则(新闻)、同心罗盘弧(主题)、上升 V 形堆叠(职业)。金色 7%,从右下角文字后面溢出。它们的存在是为了在缩略图尺寸下给构图重量,并且太淡,不会影响文字对比度。

装饰位于 .anchor-wrap 内部,一个用 overflow: hidden 固定到画布的盒子。没有它,绝对定位的图形会超出底部边缘,计入 body.scrollHeight,溢出守卫会以恒定的 160px 页面溢出拒绝每次渲染。

标志

src/templates/assets/logo.png 是提供的组合标志:星形/飞机标记在“WE ONE AVIATION”字标上方。头部用 Cinzel 自己渲染该字标,所以 scripts/prepare-logo.mjs 派生 logo-mark.png——仅标记——以避免品牌名称出现两次。它找到非透明像素的水平带并保留最高的,所以以其他分辨率重新导出标志仍然有效。替换 logo.png 后:

npm run prepare-logo

浏览器生命周期和内存

一个 Chromium 在进程生命周期内共享,仅在断开连接时重新启动。启动大约需要一秒和几百 MB,对于每篇帖子来说太昂贵了。

渲染内存。 Chromium 在 Node 之上大约需要 400 MB 常驻内存。免费实例是 512 MB,会因此 OOM——部署在渲染中途死亡,没有有用的日志行。使用 Starter 计划。 如果必须留在免费版,预期会重启,并将每次重启后的第一次渲染视为冷启动。

二进制文件来自哪里取决于主机:

主机

来源

设置了 CHROMIUM_EXECUTABLE_PATH

该路径,始终优先

Linux(Render)

@sparticuz/chromium,自带二进制文件,所以构建时无需下载浏览器

macOS / 开发

playwright-core 已缓存的任何内容(npx playwright-core install chromium)

刻意不使用 --single-process。 它与重用单个浏览器不兼容:在该标志下关闭 BrowserContext 会销毁整个浏览器,所以第二次渲染会失败,错误为 "Target page, context or browser has been closed"。在此代码库上测量:使用它时 3 个上下文中只有 1 个存活,不使用则 3/3 存活。重用是交易中更有价值的一半。如果主机要求,设置 CHROMIUM_SINGLE_PROCESS=1 强制重新启用,并预期每次启动只渲染一次。

错误处理

代码

含义

bad_input

参数验证失败。

duplicate_topic

主题已存在。按设计工作——换一个。

not_found

没有该 history_id 的 posts 行。是否调用了 reserve_topic?

db_error / storage_error

Supabase 拒绝了。details 携带 Postgres 代码。

image_generation_failed / image_too_large

提供商或 sharp 问题。

image_url_unreachable

Meta 将获取的 URL 不是可访问的 JPEG。

caption_too_long

仅标题正文超过 2200 字符。标签自动修剪;正文从不修剪。

meta_error

Graph API。details 有 code、error_subcode、type、fbtrace_id,不变。

timeout

某物超出其预算(图片 60 秒,容器轮询 60 秒,Graph 30 秒)。

config_error

缺少必需的环境变量。retryable: false。

Meta 代码 190 和 200 永不重试。 190 是过期或无效的 token,200 是缺少权限;两者都需要人工干预,重试只会消耗速率限制并隐藏真正原因。这些错误返回 retryable: false 和 needs_human 注释,说明该做什么。

每次工具调用记录 tool.start 和 tool.ok/tool.error 及持续时间,每次 Graph 调用记录 graph.call 及方法、端点、状态和耗时毫秒——所以 Render 的日志查看器足以重建运行。

故障排除

症状

原因

每次请求都返回 401

缺少 Header,或令牌与 MCP_AUTH_TOKEN 不匹配。

/mcp 返回 500 config_error,/health 正常

服务上未设置 MCP_AUTH_TOKEN。

image_url_unreachable

post-images 存储桶未设为公开,或上传静默失败。运行 npm run smoke。

IG 容器在 IN_PROGRESS 状态卡住 60 秒

Meta 无法获取图片,或响应缓慢。先在浏览器中检查该 URL。

meta_error 代码 190

令牌已过期。生成一个新的长期 Page 令牌。check_token() 会在剩余 ≤7 天时发出警告。

meta_error 代码 200

缺少权限范围 — 重新授予 instagram_content_publish / pages_manage_posts。

duplicate_topic

不是 bug。该主题已存在于 posts 中。

当天首次调用超时

免费套餐冷启动。先访问 /health,或迁移到 starter。

渲染中途进程退出,无错误信息

内存不足。Chromium 需要约 400 MB;免费实例为 512 MB。请迁移到 starter。

bad_input 指向某个字段

符合预期。缩短该字段名并重新调用 render_post — 未上传任何内容。

Self-hosted fonts failed to load

src/templates/fonts/ 未复制到 dist/。重新运行 npm run build;渲染会被拒绝,而不会以回退衬线字体输出。

Target page, context or browser has been closed

已设置 CHROMIUM_SINGLE_PROCESS=1。该标志每次启动只允许一次渲染 — 请取消它。

Render 上找不到模板文件

跳过了 npm run build,导致缺少 dist/templates/。

布局

src/
  server.ts            Express, bearer auth, /health, POST /mcp
  config.ts            Lazy env resolution, constants
  log.ts               Timed stdout logging
  errors.ts            AppError / MetaError, the no-retry rule for 190 & 200
  supabase.ts          posts CRUD + storage upload
  meta.ts              Graph client, IG container flow, FB photos, debug_token
  image.ts             Chromium lifecycle, template render, overflow guard, sharp
  tools/
    register.ts        Timing, error envelope, content-block shaping
    get_past_topics.ts reserve_topic.ts render_post.ts
    publish_socials.ts mark_draft.ts check_token.ts
    index.ts
  templates/
    tokens.css base.css          design tokens + shared skeleton
    news.html subject.html career.html
    fonts/     barlow-400/600/700, cinzel-600 (woff2, self-hosted)
    assets/    logo.png (supplied), logo-mark.png (derived)
migrations/001_init.sql
scripts/
  smoke.ts           render all three, upload, prove both guards fire
  copy-templates.mjs build step: tsc emits only .ts, templates must reach dist/
  prepare-logo.mjs   derives logo-mark.png from logo.png
render.yaml

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    MCP server to safely publish posts to multiple Facebook Pages via Meta Graph API, with built-in guardrails for brand voice, banned topics, image requirements, and anti-duplication.
    4
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    Enables reading, publishing, commenting on, and analyzing Instagram Business/Creator accounts through the official Graph API, with multi-account support and an optional self-hosted publishing panel.
    29
    MIT