commit b05d3399bfc1d74fb99d821c11aa40002c060f2e
parent f1beea234e970ccf4f858435efd7bfce6cfbf5a5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 2 Oct 2026 10:05:35 -0400
mcp: get_post names the post's archive page (`- archive: <origin>/?v=<channel>%2F<id>&vm=post`) when the source has a viewer origin -- the channel's member site on a hub, the site's own otherwise; viewerPostUrl in common/lib/momentUrl.ts
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
4 files changed, 72 insertions(+), 3 deletions(-)
diff --git a/common/lib/momentUrl.ts b/common/lib/momentUrl.ts
@@ -101,6 +101,28 @@ export function viewerMomentUrl(
return u.toString();
}
+// An archived post's page on the viewer: the post modal for `<channel>/<id>`
+// (`?v=<slug>&vm=post`), which shows the post and links on to the original.
+// Null when the origin or slug is missing or the origin can't be parsed.
+export function viewerPostUrl(
+ siteOrigin: string | null | undefined,
+ slug: string | null | undefined,
+): string | null {
+ if (!siteOrigin || !slug) return null;
+ let u: URL;
+ try {
+ u = new URL(siteOrigin);
+ } catch {
+ return null;
+ }
+ u.pathname = "/";
+ u.search = "";
+ u.hash = "";
+ u.searchParams.set("v", slug);
+ u.searchParams.set("vm", "post");
+ return u.toString();
+}
+
// The preferred moment link: viewer deep link when we have an origin+slug,
// else the platform fallback. Null when neither can be built.
export function momentUrl(input: MomentUrlInput): string | null {
diff --git a/mcp/src/momentUrl.test.ts b/mcp/src/momentUrl.test.ts
@@ -7,6 +7,7 @@ import {
momentBaseUrl,
viewerMomentBaseUrl,
platformMomentBaseUrl,
+ viewerPostUrl,
} from "yt-dlp-transcript-common/lib/momentUrl";
// ─── Archilyzer viewer link (preferred) ───
@@ -217,3 +218,23 @@ test("momentBaseUrl: viewer base wins, platform fallback, else null", () => {
assert.equal(momentBaseUrl({ siteOrigin: null, slug: "ch/vid" }), null);
});
+
+// ─── An archived post's page ───
+
+test("viewerPostUrl: origin + post slug → ?v=<channel>%2F<id>&vm=post", () => {
+ assert.equal(
+ viewerPostUrl("https://jasolyzer.pages.dev", "piratesoftware-bsky/3l6uxj6esfr2z"),
+ "https://jasolyzer.pages.dev/?v=piratesoftware-bsky%2F3l6uxj6esfr2z&vm=post",
+ );
+ // An origin with its own path is normalized to the viewer's root.
+ assert.equal(
+ viewerPostUrl("https://site.example/search?q=x", "ch-x/123"),
+ "https://site.example/?v=ch-x%2F123&vm=post",
+ );
+});
+
+test("viewerPostUrl: no origin, no slug, or a bad origin → null", () => {
+ assert.equal(viewerPostUrl(null, "a/b"), null);
+ assert.equal(viewerPostUrl("https://x.example", ""), null);
+ assert.equal(viewerPostUrl("not a url", "a/b"), null);
+});
diff --git a/mcp/src/search.test.ts b/mcp/src/search.test.ts
@@ -1192,6 +1192,28 @@ test("server: get_post returns the post with no timestamps", async () => {
await client.close();
});
+test("server: get_post names the post's archive page when the source has a viewer", async () => {
+ // A stub has no viewer origin: no archive line.
+ const bare = await connectClient(new StubSource());
+ assert.doesNotMatch(firstText(await bare.callTool({ name: "get_post", arguments: { post_id: "p1" } })), /- archive:/);
+ await bare.close();
+ // A site with one: its post modal for <channel>/<id>.
+ class SiteSource extends StubSource {
+ publicOrigin(): string | null {
+ return "https://site.example";
+ }
+ }
+ const site = await connectClient(new SiteSource());
+ const out = firstText(await site.callTool({ name: "get_post", arguments: { post_id: "p1" } }));
+ const line = /- archive: (\S+)/.exec(out);
+ assert.ok(line, "archive line present");
+ const u = new URL(line![1]);
+ assert.equal(u.origin, "https://site.example");
+ assert.equal(u.searchParams.get("vm"), "post");
+ assert.match(u.searchParams.get("v") ?? "", /\/p1$/);
+ await site.close();
+});
+
test("server: get_thread returns parent + replies", async () => {
const client = await connectClient(new StubSource());
const res = await client.callTool({
diff --git a/mcp/src/server.ts b/mcp/src/server.ts
@@ -8,7 +8,7 @@ import { transcriptToMarkdown } from "yt-dlp-transcript-common/lib/transcriptToM
import { formatDate, formatDuration } from "yt-dlp-transcript-common/lib/format";
import { manifestHasDigest } from "yt-dlp-transcript-common/lib/digests";
import type { VideoStat } from "yt-dlp-transcript-common/lib/stats";
-import { momentUrl, momentBaseUrl } from "yt-dlp-transcript-common/lib/momentUrl";
+import { momentUrl, momentBaseUrl, viewerPostUrl } from "yt-dlp-transcript-common/lib/momentUrl";
import type { Platform } from "yt-dlp-transcript-common/lib/platform";
import type { SearchAlias } from "yt-dlp-transcript-common/lib/searchAliases";
import { isTagId, type PublishedTag } from "yt-dlp-transcript-common/lib/curatedTags";
@@ -2136,7 +2136,7 @@ async function handleGetTranscripts(
// Render one post as markdown. No timestamps anywhere — a post has no timeline,
// and emitting a 0:00 would invite a bogus `@ mm:ss` citation.
-function postToMarkdown(post: Post, heading = true): string {
+function postToMarkdown(post: Post, heading = true, archive: string | null = null): string {
const lines: string[] = [];
if (heading) lines.push(`# Post by ${post.authorName || post.author}`);
lines.push(
@@ -2146,6 +2146,9 @@ function postToMarkdown(post: Post, heading = true): string {
lines.push(`- platform: ${post.platform}`);
lines.push(`- post_id: ${post.id}`);
lines.push(`- source: ${post.url}`);
+ // The post's page on the archive, when the source has a viewer: a citation
+ // that survives the post, or the platform, going away.
+ if (archive) lines.push(`- archive: ${archive}`);
if (post.isRepost) lines.push(`- repost: yes`);
if (post.isReply) lines.push(`- reply: yes`);
if (post.threadId && post.threadId !== post.id) {
@@ -2186,7 +2189,8 @@ async function handleGetPost(
typeof args.channel === "string" ? args.channel : undefined,
);
if (!found) return errorText(`post not found: ${postId}`);
- return text(postToMarkdown(found.post));
+ const archive = viewerPostUrl(found.ch.siteUrl ?? source.publicOrigin(), found.post.slug);
+ return text(postToMarkdown(found.post, true, archive));
}
async function handleGetThread(