Skip to content

Commit cfc79bb

Browse files
committed
feat: add get_post / sideshow show for single-post reads
Adds a read primitive for fetching a single post by id across all three tiers — the missing counterpart to list_posts / sideshow list. MCP: get_post(id) — returns the full post (surfaces with ids, version, history). Available on both stdio and HTTP transports. CLI: sideshow show <id> — prints the full post JSON. Agents need this to recover surface ids for per-surface operations (edit_surface, remove_surface, reorder_surfaces) after a context compaction, without listing the entire session.
1 parent ac27d21 commit cfc79bb

6 files changed

Lines changed: 116 additions & 0 deletions

File tree

bin/sideshow.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ usage:
127127
--surface is a deprecated alias)
128128
--author <name> defaults to agent name
129129
sideshow list [--session <id>|--all] list posts
130+
sideshow show <id> show a single post (surfaces, ids, version, history)
130131
sideshow sessions list sessions
131132
sideshow demo seed two example sessions to explore the viewer
132133
sideshow guide print the design contract for posts
@@ -1520,6 +1521,13 @@ const commands = {
15201521
out(await api(`/api/sessions/${session}/surfaces`));
15211522
},
15221523

1524+
async show() {
1525+
const { positionals } = parse({ allowPositionals: true });
1526+
const id = positionals[0];
1527+
if (!id) fail("usage: sideshow show <id>");
1528+
out(await api(`/api/posts/${id}`));
1529+
},
1530+
15231531
async sessions() {
15241532
parse();
15251533
out(await api("/api/sessions"));

mcp/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ server.registerTool(
9999
},
100100
);
101101

102+
server.registerTool(
103+
"get_post",
104+
{
105+
description: MCP_TOOL_DESCRIPTIONS.getPost,
106+
inputSchema: STDIO_MCP_INPUT_SCHEMAS.getPost,
107+
},
108+
async ({ id }) => {
109+
return text(JSON.parse(await api(`/api/posts/${id}`)));
110+
},
111+
);
112+
102113
server.registerTool(
103114
"publish_surface",
104115
{

server/mcpHttp.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ export function registerMcp(app: Hono, deps: McpDeps) {
195195
2,
196196
);
197197
}
198+
case "get_post": {
199+
const post = await deps.store.getPost(String(args.id ?? ""));
200+
if (!post) throw new Error("post not found");
201+
return JSON.stringify(post, null, 2);
202+
}
198203
case "upload_asset": {
199204
if (typeof args.data !== "string" || args.data.length === 0) {
200205
throw new Error("upload_asset needs base64 `data`");

server/mcpSpec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ export const MCP_TOOL_DESCRIPTIONS = {
163163
"Revise a post in place (same card, new version). Prefer this over publishing a near-duplicate. Pass the full replacement surfaces array. If the result includes userFeedback, read it.",
164164
listPostsHttp: "List posts — pass a session id to scope, or omit for all sessions.",
165165
listPostsStdio: "List posts in this conversation's session.",
166+
getPost:
167+
"Fetch a single post by id — returns the full post object including surfaces (with their ids), version, and history. Use this to recover surface ids for per-surface operations (edit_surface, remove_surface, reorder_surfaces) after a context compaction, or to inspect a post's current state before editing.",
166168
publishSurfaceHttp:
167169
"Deprecated alias of publish_post — Publish a post to the user's sideshow workspace. A post is an ordered list of surfaces (html, markdown, mermaid, diff, image, trace, terminal, json, code). Returns the post id, view URL, and sessionId — pass sessionId as `session` on later calls. On your first publish, pass sessionTitle naming the task. If the result includes userFeedback, those are new comments from the user. Call get_design_guide first if you have not this session.",
168170
publishSurfaceStdio:
@@ -234,6 +236,17 @@ export const HTTP_MCP_TOOLS = [
234236
},
235237
},
236238
},
239+
{
240+
name: "get_post",
241+
description: MCP_TOOL_DESCRIPTIONS.getPost,
242+
inputSchema: {
243+
type: "object",
244+
properties: {
245+
id: { type: "string", description: d.surfaceId },
246+
},
247+
required: ["id"],
248+
},
249+
},
237250
{
238251
name: "publish_surface",
239252
description: MCP_TOOL_DESCRIPTIONS.publishSurfaceHttp,
@@ -470,6 +483,9 @@ export const STDIO_MCP_INPUT_SCHEMAS = {
470483
surfaces: z.array(mcpPartSchema).optional().describe(d.replacementParts),
471484
title: z.string().optional().describe(d.replacementTitle),
472485
},
486+
getPost: {
487+
id: z.string().describe(d.surfaceId),
488+
},
473489
publishSurface: {
474490
title: z.string().describe(d.title),
475491
parts: z.array(mcpPartSchema).describe(MCP_SURFACES_DESCRIPTION),

test/api.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2949,3 +2949,49 @@ test("mcp tools/list includes the new per-surface tools", async () => {
29492949
assert.ok(names.includes("remove_surface"));
29502950
assert.ok(names.includes("reorder_surfaces"));
29512951
});
2952+
2953+
test("mcp get_post fetches a single post with surface ids via HTTP MCP", async () => {
2954+
const app = makeApp();
2955+
const pub = (await (
2956+
await app.request(
2957+
"/mcp",
2958+
mcpCall(1, "tools/call", {
2959+
name: "publish_post",
2960+
arguments: {
2961+
title: "GetPost",
2962+
surfaces: [
2963+
{ kind: "html", html: "<p>a</p>" },
2964+
{ kind: "markdown", markdown: "# b" },
2965+
],
2966+
},
2967+
}),
2968+
)
2969+
).json()) as any;
2970+
const postId = JSON.parse(pub.result.content[0].text).id;
2971+
2972+
const res = (await (
2973+
await app.request(
2974+
"/mcp",
2975+
mcpCall(2, "tools/call", {
2976+
name: "get_post",
2977+
arguments: { id: postId },
2978+
}),
2979+
)
2980+
).json()) as any;
2981+
assert.equal(res.result.isError, undefined);
2982+
const post = JSON.parse(res.result.content[0].text);
2983+
assert.equal(post.id, postId);
2984+
assert.equal(post.title, "GetPost");
2985+
assert.equal(post.surfaces.length, 2);
2986+
assert.equal(post.surfaces[0].kind, "html");
2987+
assert.equal(post.surfaces[1].kind, "markdown");
2988+
assert.ok(post.surfaces[0].id, "surface ids are present");
2989+
assert.ok(post.surfaces[1].id);
2990+
});
2991+
2992+
test("mcp tools/list includes get_post", async () => {
2993+
const app = makeApp();
2994+
const list = (await (await app.request("/mcp", mcpCall(1, "tools/list"))).json()) as any;
2995+
const names = list.result.tools.map((t: any) => t.name);
2996+
assert.ok(names.includes("get_post"));
2997+
});

test/cli.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ for (const cmd of [
9393
"watch",
9494
"comment",
9595
"list",
96+
"show",
9697
"kits",
9798
]) {
9899
test(`${cmd} --help prints usage and exits 0`, async () => {
@@ -948,6 +949,35 @@ test("sessions prints the workspace's sessions", async () => {
948949
}
949950
});
950951

952+
test("show prints a single post with surface ids", async () => {
953+
const server = await serveSession();
954+
try {
955+
const html = tmpFile("h.html", "<p>a</p>");
956+
const md = tmpFile("m.md", "# b");
957+
const pub = await cli(server, "publish", html, "--md", md, "--title", "ShowMe");
958+
const id = JSON.parse(pub.stdout).id;
959+
960+
const { code, stdout } = await cli(server, "show", id);
961+
assert.equal(code, 0);
962+
const post = JSON.parse(stdout);
963+
assert.equal(post.id, id);
964+
assert.equal(post.title, "ShowMe");
965+
assert.equal(post.surfaces.length, 2);
966+
assert.equal(post.surfaces[0].kind, "html");
967+
assert.equal(post.surfaces[1].kind, "markdown");
968+
assert.ok(post.surfaces[0].id, "surface ids are present");
969+
assert.ok(post.surfaces[1].id);
970+
} finally {
971+
await server.close();
972+
}
973+
});
974+
975+
test("show without an id fails with a usage error", async () => {
976+
const { code, stderr } = await run("show");
977+
assert.notEqual(code, 0);
978+
assert.match(stderr, /usage: sideshow show/);
979+
});
980+
951981
// --- assets (image / upload / asset-url) ----------------------------------
952982

953983
test("image uploads bytes and publishes an image post", async () => {

0 commit comments

Comments
 (0)