Skip to content

Add WebMCP tool exposure to AsyncTalk#59

Merged
AnnatarHe merged 1 commit intomasterfrom
codex/implement-webmcp-tools
Apr 19, 2026
Merged

Add WebMCP tool exposure to AsyncTalk#59
AnnatarHe merged 1 commit intomasterfrom
codex/implement-webmcp-tools

Conversation

@AnnatarHe
Copy link
Copy Markdown
Member

Summary

  • Add a site-wide WebMCP component that registers AsyncTalk tools on page load.
  • Expose latest/search/get/open episode actions plus subscription links from browser AI agents.
  • Support both provideContext({ tools }) and registerTool(...) with abort cleanup.

Testing

  • pnpm astro check
  • pnpm build

@AnnatarHe AnnatarHe temporarily deployed to codex/implement-webmcp-tools - asynctalk-website PR #59 April 19, 2026 03:28 — with Render Destroyed
@claude
Copy link
Copy Markdown

claude Bot commented Apr 19, 2026

Code Review: Add WebMCP Tool Exposure

Overview: This PR adds WebMCP.astro, a site-wide component that exposes AsyncTalk episode data to browser AI agents via navigator.modelContext. It registers five read-only (plus one navigation) tools and is injected into the global layout. A solid idea, and the implementation is generally clean — a few items worth addressing.


Security Concerns

Draft/pending episodes embedded in every page (medium severity)
All episodes — including those with status: "draft" or "pending" — are serialized and embedded in every page's HTML. The publicEpisodes() filter hides them from default tool responses, but any visitor can read the page source or pass includePrerelease: true to an AI agent to access their titles, previews, and metadata.

// Consider filtering at build time so drafts never reach the browser:
const episodes = posts
  .filter((post) => post.data.status === "published") // or allow "pending" explicitly
  .map<EpisodeToolData>(...)

If draft content is intentionally confidential, pre-filter it in the Astro frontmatter before serializing.

serializeForInlineScript only escapes <
Escaping < to \u003c correctly prevents </script> injection. For full defense-in-depth, > (\u003e) and & (\u0026) are also worth escaping — they're harmless in JSON string values today, but the function is a general utility and broader escaping makes it robust by convention.


Performance Considerations

All episode data inlined on every page
With ~48 episodes × ~500-char previews, the inline payload adds roughly 30–50 KB (uncompressed) to every page. This is loaded even when the visitor has no AI agent (navigator.modelContext is undefined), though the early return limits JS execution overhead.

A few options to consider:

  • Move the data into a separately-fetched JSON endpoint (/api/episodes.json) and lazy-load it only when navigator.modelContext is detected.
  • Reduce the preview truncation (currently 500 chars); 200–300 chars is usually enough for AI context.

Code Quality

normalizeId / normalizeEpisodeId duplication
normalizeId runs server-side in the frontmatter, and a nearly identical normalizeEpisodeId is redefined inside the inline script. Since they serve different purposes (build-time vs. client-time), this is acceptable — but a comment noting the intentional duplication would prevent future readers from trying to "fix" it.

findEpisode has a redundant condition

episode.slug === input.slug  // if input.slug was used to derive `id`, this overlaps with the third check
episode.slug === `/posts/${id}`

When input.id is absent, id is derived from normalizeEpisodeId(input.slug), making the second and third checks equivalent for most inputs. Not a bug, just noisy.

searchEpisodes: schema says query is required, but execute re-validates
The runtime if (!query) throw guard is fine defensively, but since the inputSchema already marks query as required, compliant callers will never hit it. Consider either keeping it as a silent safeguard (fine) or documenting why it's there.

openEpisode is missing readOnlyHint: false
The four read tools correctly set annotations: { readOnlyHint: true }. openEpisode omits annotations entirely — explicitly setting readOnlyHint: false would make the intent clear to agents that inspect tool metadata.


Minor / Conventions

  • Language of tool descriptions: CLAUDE.md specifies Chinese for all UI copy, but MCP tool descriptions target AI models rather than human readers. English is arguably more appropriate here since models are trained on English-heavy data — worth a quick team decision to document the rationale either way.
  • outputSchema not provided: Several WebMCP implementations support an outputSchema alongside inputSchema. If the spec you're targeting supports it, adding one for structuredContent would help agents validate responses.
  • stripMarkdown edge cases: The regex won't handle HTML blocks inside MDX (e.g., <details> tags) or JSX component calls. For a 500-char preview this is likely fine, but worth noting if episodes start embedding more complex MDX.

Summary

Area Status
Core logic ✅ Correct and well-structured
Draft episode exposure ⚠️ Review intentionality
Bundle size ⚠️ Consider lazy-loading
XSS / injection ✅ Handled (minor hardening possible)
Abort cleanup ✅ Good pagehide cleanup
Fallback for missing API ✅ Both provideContext and registerTool covered

The implementation is well-thought-out. The draft episode exposure and bundle size are the two items most worth deciding on before merge. Everything else is polish.

@AnnatarHe AnnatarHe merged commit cb4b3fb into master Apr 19, 2026
1 check passed
@AnnatarHe AnnatarHe deleted the codex/implement-webmcp-tools branch April 19, 2026 03:30
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb43cd3a1a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

throw new Error("Episode not found.");
}

window.location.assign(episode.url);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Navigate openEpisode on current origin

openEpisode currently does window.location.assign(episode.url), but episode.url is precomputed from Astro.site (the canonical production domain), not the page’s runtime origin. In non-production contexts (localhost, preview deploys, staging domains), invoking this tool will jump users to production instead of navigating within the current environment, which breaks agent-assisted testing and can lose same-origin state. Use episode.slug (relative navigation) or rebuild the URL from window.location.origin at call time.

Useful? React with 👍 / 👎.

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.

1 participant