Skip to content

Commit 9c15304

Browse files
ochafikclaude
andcommitted
pdf-server: improve interact tool annotation discoverability
- Add concrete per-type schema docs with field names in tool description - Add JSON example showing add_annotations with highlight + stamp - Replace opaque z.record(z.string(), z.unknown()) with typed union of all annotation schemas (full + partial forms) so the model sees exact field names and types - Remove redundant manual safeParse since Zod inputSchema validates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cd7d107 commit 9c15304

1 file changed

Lines changed: 49 additions & 49 deletions

File tree

examples/pdf-server/server.ts

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -872,16 +872,29 @@ Actions:
872872
- find: Search text silently (no UI change). Requires \`query\`. Results appear in model context only.
873873
- search_navigate: Jump to a search match. Requires \`matchIndex\` (from search/find results).
874874
- zoom: Set zoom level. Requires \`scale\` (0.5–3.0).
875-
- add_annotations: Add annotations to the PDF. Requires \`annotations\` array.
876-
- update_annotations: Partially update existing annotations. Requires \`annotations\` array (id + type required).
875+
- add_annotations: Add annotations to the PDF. Requires \`annotations\` array. Each annotation has \`id\` (string), \`type\`, and \`page\` (1-indexed).
876+
- update_annotations: Partially update existing annotations. Requires \`annotations\` array (id + type required, other fields optional).
877877
- remove_annotations: Remove annotations by ID. Requires \`ids\` array.
878-
- highlight_text: Find text and highlight it. Requires \`query\`. Optional \`page\`, \`color\`, \`content\`.
878+
- highlight_text: Find text and highlight it. Requires \`query\`. Optional \`page\` (defaults to all pages), \`color\`, \`content\` (tooltip).
879879
- fill_form: Fill form fields. Requires \`fields\` array of { name, value }.
880880
- get_pages: Get text and/or screenshots from pages without navigating. Uses \`intervals\` (page ranges with optional start/end, e.g. [{start:1,end:5}], [{}] for all). Optional \`getText\` (default true), \`getScreenshots\` (default false). Max 20 pages. Returns page content directly.
881881
882-
Annotation types: highlight, underline, strikethrough, note, rectangle, freetext, stamp.
883-
Coordinates are in PDF points (72 dpi), bottom-left origin. Colors are CSS strings (e.g. "#ff0000", "rgba(255,0,0,0.5)").
884-
Stamp labels: APPROVED, DRAFT, CONFIDENTIAL, FINAL, VOID, REJECTED.`,
882+
Annotation types (all use PDF points, 72 dpi, bottom-left origin; colors are CSS strings):
883+
- highlight: \`{id, type:"highlight", page, rects:[{x,y,width,height}], color?, content?}\` — yellow semi-transparent overlay
884+
- underline: \`{id, type:"underline", page, rects:[{x,y,width,height}], color?}\` — red underline
885+
- strikethrough: \`{id, type:"strikethrough", page, rects:[{x,y,width,height}], color?}\` — line through text
886+
- note: \`{id, type:"note", page, x, y, content, color?}\` — sticky note icon with tooltip
887+
- rectangle: \`{id, type:"rectangle", page, x, y, width, height, color?, fillColor?}\` — box outline/fill
888+
- freetext: \`{id, type:"freetext", page, x, y, content, fontSize?, color?}\` — text at position
889+
- stamp: \`{id, type:"stamp", page, x, y, label, color?, rotation?}\` — label is one of: APPROVED, DRAFT, CONFIDENTIAL, FINAL, VOID, REJECTED
890+
891+
Example — add a highlight and a stamp:
892+
\`\`\`json
893+
{"action":"add_annotations","viewUUID":"...","annotations":[
894+
{"id":"h1","type":"highlight","page":1,"rects":[{"x":72,"y":700,"width":200,"height":12}],"color":"rgba(255,255,0,0.5)"},
895+
{"id":"s1","type":"stamp","page":1,"x":300,"y":500,"label":"APPROVED","color":"#00aa00","rotation":-15}
896+
]}
897+
\`\`\``,
885898
inputSchema: {
886899
viewUUID: z
887900
.string()
@@ -922,10 +935,31 @@ Stamp labels: APPROVED, DRAFT, CONFIDENTIAL, FINAL, VOID, REJECTED.`,
922935
.optional()
923936
.describe("Zoom scale, 1.0 = 100% (for zoom)"),
924937
annotations: z
925-
.array(z.record(z.string(), z.unknown()))
938+
.array(
939+
z.union([
940+
HighlightAnnotation,
941+
UnderlineAnnotation,
942+
StrikethroughAnnotation,
943+
NoteAnnotation,
944+
RectangleAnnotation,
945+
FreetextAnnotation,
946+
StampAnnotation,
947+
// Partial forms for update_annotations (id + type required)
948+
HighlightAnnotation.partial().required({ id: true, type: true }),
949+
UnderlineAnnotation.partial().required({ id: true, type: true }),
950+
StrikethroughAnnotation.partial().required({
951+
id: true,
952+
type: true,
953+
}),
954+
NoteAnnotation.partial().required({ id: true, type: true }),
955+
RectangleAnnotation.partial().required({ id: true, type: true }),
956+
FreetextAnnotation.partial().required({ id: true, type: true }),
957+
StampAnnotation.partial().required({ id: true, type: true }),
958+
]),
959+
)
926960
.optional()
927961
.describe(
928-
"Annotation definitions (for add_annotations) or partial updates (for update_annotations)",
962+
"Annotation objects for add_annotations (full) or update_annotations (partial, id+type required).",
929963
),
930964
ids: z
931965
.array(z.string())
@@ -1031,7 +1065,7 @@ Stamp labels: APPROVED, DRAFT, CONFIDENTIAL, FINAL, VOID, REJECTED.`,
10311065
enqueueCommand(uuid, { type: "zoom", scale });
10321066
description = `zoom to ${Math.round(scale * 100)}%`;
10331067
break;
1034-
case "add_annotations": {
1068+
case "add_annotations":
10351069
if (!annotations || annotations.length === 0)
10361070
return {
10371071
content: [
@@ -1042,31 +1076,14 @@ Stamp labels: APPROVED, DRAFT, CONFIDENTIAL, FINAL, VOID, REJECTED.`,
10421076
],
10431077
isError: true,
10441078
};
1045-
// Validate each annotation
1046-
const parsed = [];
1047-
for (const raw of annotations) {
1048-
const result = PdfAnnotationDef.safeParse(raw);
1049-
if (!result.success) {
1050-
return {
1051-
content: [
1052-
{
1053-
type: "text",
1054-
text: `Invalid annotation: ${result.error.message}`,
1055-
},
1056-
],
1057-
isError: true,
1058-
};
1059-
}
1060-
parsed.push(result.data);
1061-
}
1079+
// annotations are already validated by Zod inputSchema
10621080
enqueueCommand(uuid, {
10631081
type: "add_annotations",
1064-
annotations: parsed,
1082+
annotations: annotations as z.infer<typeof PdfAnnotationDef>[],
10651083
});
1066-
description = `add ${parsed.length} annotation(s)`;
1084+
description = `add ${annotations.length} annotation(s)`;
10671085
break;
1068-
}
1069-
case "update_annotations": {
1086+
case "update_annotations":
10701087
if (!annotations || annotations.length === 0)
10711088
return {
10721089
content: [
@@ -1077,29 +1094,12 @@ Stamp labels: APPROVED, DRAFT, CONFIDENTIAL, FINAL, VOID, REJECTED.`,
10771094
],
10781095
isError: true,
10791096
};
1080-
const parsedUpdates = [];
1081-
for (const raw of annotations) {
1082-
const result = PdfAnnotationUpdate.safeParse(raw);
1083-
if (!result.success) {
1084-
return {
1085-
content: [
1086-
{
1087-
type: "text",
1088-
text: `Invalid annotation update: ${result.error.message}`,
1089-
},
1090-
],
1091-
isError: true,
1092-
};
1093-
}
1094-
parsedUpdates.push(result.data);
1095-
}
10961097
enqueueCommand(uuid, {
10971098
type: "update_annotations",
1098-
annotations: parsedUpdates,
1099+
annotations: annotations as z.infer<typeof PdfAnnotationUpdate>[],
10991100
});
1100-
description = `update ${parsedUpdates.length} annotation(s)`;
1101+
description = `update ${annotations.length} annotation(s)`;
11011102
break;
1102-
}
11031103
case "remove_annotations":
11041104
if (!ids || ids.length === 0)
11051105
return {

0 commit comments

Comments
 (0)