Calling tools
A typed client in TypeScript, Python or Go, and the most common tool calls.
Every tool is POST /v1/tools/<name> with the tool’s input as a JSON body. The answer is the tool’s result as JSON. This page sets up a small client and walks through the most common calls; the endpoint reference has every tool.
Set up a client
The TypeScript, Python and Go examples share one small client file: save it in the folder where you keep your scripts. It sends the token with every request, encodes and decodes JSON, and turns error answers into an ApiError (in Go, a *Problem error) carrying the problem details. Its types follow openapi.json. curl needs no setup.
// A typed client for the superPDF API. Types follow openapi.json.
const BASE_URL = 'http://127.0.0.1:7341';
const TOKEN = process.env.SUPERPDF_API_TOKEN;
if (!TOKEN) throw new Error('Set SUPERPDF_API_TOKEN to your API token.');
/** A new PDF written by a tool. */
export interface NewFile {
output: string;
pages: number;
bytes: number;
}
export interface Tool {
name: string;
title: string;
description: string;
reports_progress: boolean;
annotations: Record<string, boolean>;
}
/** A heading in the outline, with the pages it covers. */
export interface Section {
id: string;
title: string;
level: number;
startPage: number;
endPage: number;
children: Section[];
}
export interface Structure {
format: string;
total_pages: number;
has_text: boolean;
truncated: boolean;
content: string;
sections: Section[];
}
export interface BatchResult {
outputs: (NewFile & { input: string })[];
failed: { input: string; error: string }[];
}
/** RFC 9457 problem details: the body of every error answer. */
export interface Problem {
type: string;
title: string;
status: number;
detail: string;
instance: string;
code: string;
request_id: string;
errors?: { field: string; message: string }[];
}
export interface Job<T = unknown> {
id: string;
tool: string;
status: 'queued' | 'running' | 'succeeded' | 'failed';
progress: { progress: number; total: number; message?: string } | null;
created_at: string;
started_at: string | null;
finished_at: string | null;
result?: T;
error?: Problem;
callback?: {
url: string;
status: 'pending' | 'delivering' | 'delivered' | 'failed';
attempts: number;
last_status: number | null;
};
}
/** Thrown for every answer that isn't 2xx. */
export class ApiError extends Error {
readonly problem: Problem;
constructor(problem: Problem) {
super(`${problem.status} ${problem.code}: ${problem.detail}`);
this.problem = problem;
}
}
export async function request<T>(
method: 'GET' | 'POST',
path: string,
body?: object,
): Promise<T> {
const response = await fetch(BASE_URL + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
},
body: body && JSON.stringify(body),
});
const data = await response.json();
if (!response.ok) throw new ApiError(data);
return data;
}
export function callTool<T>(name: string, input: object): Promise<T> {
return request<T>('POST', `/v1/tools/${name}`, input);
}
"""A typed client for the superPDF API. Types follow openapi.json."""
import os
from typing import Any, Literal, NotRequired, TypedDict
import httpx
BASE_URL = "http://127.0.0.1:7341"
class NewFile(TypedDict):
"""A new PDF written by a tool."""
output: str
pages: int
bytes: int
class Tool(TypedDict):
name: str
title: str
description: str
reports_progress: bool
annotations: dict[str, bool]
class Section(TypedDict):
"""A heading in the outline, with the pages it covers."""
id: str
title: str
level: int
startPage: int
endPage: int
children: list["Section"]
class Structure(TypedDict):
format: str
total_pages: int
has_text: bool
truncated: bool
content: str
sections: list[Section]
class BatchOutput(NewFile):
input: str
class BatchFailure(TypedDict):
input: str
error: str
class BatchResult(TypedDict):
outputs: list[BatchOutput]
failed: list[BatchFailure]
class FieldError(TypedDict):
field: str
message: str
class Problem(TypedDict):
"""RFC 9457 problem details: the body of every error answer."""
type: str
title: str
status: int
detail: str
instance: str
code: str
request_id: str
errors: NotRequired[list[FieldError]]
class Progress(TypedDict):
progress: float
total: float
message: NotRequired[str]
class Callback(TypedDict):
url: str
status: Literal["pending", "delivering", "delivered", "failed"]
attempts: int
last_status: int | None
class Job(TypedDict):
id: str
tool: str
status: Literal["queued", "running", "succeeded", "failed"]
progress: Progress | None
created_at: str
started_at: str | None
finished_at: str | None
result: NotRequired[Any]
error: NotRequired[Problem]
callback: NotRequired[Callback]
class ApiError(Exception):
"""Raised for every answer that isn't 2xx."""
def __init__(self, problem: Problem) -> None:
message = f"{problem['status']} {problem['code']}: {problem['detail']}"
super().__init__(message)
self.problem = problem
_client = httpx.Client(
base_url=BASE_URL,
headers={"Authorization": f"Bearer {os.environ['SUPERPDF_API_TOKEN']}"},
timeout=300, # tools can take minutes on large files
)
def request(method: str, path: str, body: dict[str, Any] | None = None) -> Any:
response = _client.request(method, path, json=body)
if response.is_error:
raise ApiError(response.json())
return response.json()
def call_tool(name: str, arguments: dict[str, Any]) -> Any:
return request("POST", f"/v1/tools/{name}", arguments)
// A typed client for the superPDF API. Types follow openapi.json.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const baseURL = "http://127.0.0.1:7341"
// NewFile is a new PDF written by a tool.
type NewFile struct {
Output string `json:"output"`
Pages int `json:"pages"`
Bytes int `json:"bytes"`
}
type Tool struct {
Name string `json:"name"`
Title string `json:"title"`
Description string `json:"description"`
ReportsProgress bool `json:"reports_progress"`
Annotations map[string]bool `json:"annotations"`
}
// Section is a heading in the outline, with the pages it covers.
type Section struct {
ID string `json:"id"`
Title string `json:"title"`
Level int `json:"level"`
StartPage int `json:"startPage"`
EndPage int `json:"endPage"`
Children []Section `json:"children"`
}
type Structure struct {
Format string `json:"format"`
TotalPages int `json:"total_pages"`
HasText bool `json:"has_text"`
Truncated bool `json:"truncated"`
Content string `json:"content"`
Sections []Section `json:"sections"`
}
type BatchOutput struct {
NewFile
Input string `json:"input"`
}
type BatchFailure struct {
Input string `json:"input"`
Error string `json:"error"`
}
type BatchResult struct {
Outputs []BatchOutput `json:"outputs"`
Failed []BatchFailure `json:"failed"`
}
type FieldError struct {
Field string `json:"field"`
Message string `json:"message"`
}
// Problem is the RFC 9457 body of every error answer. Request returns it as the error.
type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
Instance string `json:"instance"`
Code string `json:"code"`
RequestID string `json:"request_id"`
Errors []FieldError `json:"errors,omitempty"`
}
func (p *Problem) Error() string {
return fmt.Sprintf("%d %s: %s", p.Status, p.Code, p.Detail)
}
type Progress struct {
Progress float64 `json:"progress"`
Total float64 `json:"total"`
Message string `json:"message,omitempty"`
}
type Callback struct {
URL string `json:"url"`
Status string `json:"status"`
Attempts int `json:"attempts"`
LastStatus *int `json:"last_status"`
}
type Job[T any] struct {
ID string `json:"id"`
Tool string `json:"tool"`
Status string `json:"status"` // queued, running, succeeded or failed
Progress *Progress `json:"progress"`
CreatedAt string `json:"created_at"`
StartedAt *string `json:"started_at"`
FinishedAt *string `json:"finished_at"`
Result *T `json:"result,omitempty"`
Error *Problem `json:"error,omitempty"`
Callback *Callback `json:"callback,omitempty"`
}
// Tools can take minutes on large files.
var client = &http.Client{Timeout: 5 * time.Minute}
// Request sends body as JSON and decodes the answer into out.
func Request(method, path string, body, out any) error {
var payload io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return err
}
payload = bytes.NewReader(data)
}
req, err := http.NewRequest(method, baseURL+path, payload)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("SUPERPDF_API_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
problem := &Problem{}
if err := json.NewDecoder(res.Body).Decode(problem); err != nil {
return fmt.Errorf("superPDF API: HTTP %d", res.StatusCode)
}
return problem
}
return json.NewDecoder(res.Body).Decode(out)
}
// CallTool runs one tool and returns its result.
func CallTool[T any](name string, input any) (T, error) {
var result T
err := Request("POST", "/v1/tools/"+name, input, &result)
return result, err
}
The examples read the token from SUPERPDF_API_TOKEN; start the API as in Quickstart. Run each example from the folder with the client file:
# Node.js 22.18 or later runs TypeScript directly (Bun and Deno do too)
node list-tools.ts# Python 3.11 or later
pip3 install httpx
python3 list_tools.py# Go 1.22 or later; the standard library is enough
go run superpdf.go list_tools.goList tools
GET /v1/tools lists every tool with its title, description and MCP annotations. Tools with readOnlyHint only read; the others write new files.
curl http://127.0.0.1:7341/v1/tools \
-H "Authorization: Bearer $SUPERPDF_API_TOKEN"import { request, type Tool } from './superpdf.ts';
const { tools } = await request<{ tools: Tool[] }>('GET', '/v1/tools');
for (const tool of tools) console.log(tool.name.padEnd(24) + tool.title);
from superpdf import Tool, request
tools: list[Tool] = request("GET", "/v1/tools")["tools"]
for tool in tools:
print(f"{tool['name']:<24}{tool['title']}")
package main
import (
"fmt"
"log"
)
func main() {
var list struct {
Tools []Tool `json:"tools"`
}
if err := Request("GET", "/v1/tools", nil, &list); err != nil {
log.Fatal(err)
}
for _, tool := range list.Tools {
fmt.Printf("%-24s%s\n", tool.Name, tool.Title)
}
}
{
"tools": [
{
"name": "pdf_list_files",
"title": "List PDFs",
"description": "List PDF files in an allowed folder (not recursive), with siz…",
"reports_progress": false,
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
}
},
…
]
}Call a tool
The body has the same fields as the MCP tool. Relative paths are inside the first allowed folder, and results give absolute paths. This copies pages 1 to 3 of report.pdf into a new file:
curl http://127.0.0.1:7341/v1/tools/pdf_extract_pages \
-H "Authorization: Bearer $SUPERPDF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "input": "report.pdf", "pages": "1-3", "output": "summary.pdf" }'import { callTool, type NewFile } from './superpdf.ts';
const result = await callTool<NewFile>('pdf_extract_pages', {
input: 'report.pdf',
pages: '1-3',
output: 'summary.pdf',
});
console.log(`Wrote ${result.pages} pages to ${result.output}`);
from superpdf import NewFile, call_tool
result: NewFile = call_tool("pdf_extract_pages", {
"input": "report.pdf",
"pages": "1-3",
"output": "summary.pdf",
})
print(f"Wrote {result['pages']} pages to {result['output']}")
package main
import (
"fmt"
"log"
)
func main() {
result, err := CallTool[NewFile]("pdf_extract_pages", map[string]any{
"input": "report.pdf",
"pages": "1-3",
"output": "summary.pdf",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Wrote %d pages to %s\n", result.Pages, result.Output)
}
{
"output": "/Users/<you>/Documents/superPDF/summary.pdf",
"pages": 3,
"bytes": 1547
}Outputs are always new files. If summary.pdf already exists, the API answers 409 and writes nothing.
Merge PDF
pdf_merge combines 2 or more PDFs, with no upper limit, in the order you list them:
curl http://127.0.0.1:7341/v1/tools/pdf_merge \
-H "Authorization: Bearer $SUPERPDF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "inputs": ["report.pdf", "appendix.pdf"], "output": "combined.pdf" }'import { callTool, type NewFile } from './superpdf.ts';
const result = await callTool<NewFile>('pdf_merge', {
inputs: ['report.pdf', 'appendix.pdf'],
output: 'combined.pdf',
});
console.log(`Combined ${result.pages} pages into ${result.output}`);
from superpdf import NewFile, call_tool
result: NewFile = call_tool("pdf_merge", {
"inputs": ["report.pdf", "appendix.pdf"],
"output": "combined.pdf",
})
print(f"Combined {result['pages']} pages into {result['output']}")
package main
import (
"fmt"
"log"
)
func main() {
result, err := CallTool[NewFile]("pdf_merge", map[string]any{
"inputs": []string{"report.pdf", "appendix.pdf"},
"output": "combined.pdf",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Combined %d pages into %s\n", result.Pages, result.Output)
}
{
"output": "/Users/<you>/Documents/superPDF/combined.pdf",
"pages": 5,
"bytes": 2093
}Convert to Markdown
pdf_extract_structure returns a document’s text as Markdown, HTML or JSON, plus sections: its outline, from bookmarks or detected headings, with the pages each section covers. The examples save the Markdown as report.md and print the outline:
curl -s http://127.0.0.1:7341/v1/tools/pdf_extract_structure \
-H "Authorization: Bearer $SUPERPDF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "input": "report.pdf", "format": "markdown" }' \
| jq -r .content > report.mdimport { writeFile } from 'node:fs/promises';
import { callTool, type Section, type Structure } from './superpdf.ts';
const doc = await callTool<Structure>('pdf_extract_structure', {
input: 'report.pdf',
format: 'markdown',
});
await writeFile('report.md', doc.content);
// Print the outline: each heading with the pages it covers
function printOutline(sections: Section[], depth = 0): void {
for (const section of sections) {
const pages = `pages ${section.startPage}-${section.endPage}`;
console.log(`${' '.repeat(depth)}${section.title} (${pages})`);
printOutline(section.children, depth + 1);
}
}
printOutline(doc.sections);
from pathlib import Path
from superpdf import Section, Structure, call_tool
doc: Structure = call_tool("pdf_extract_structure", {
"input": "report.pdf",
"format": "markdown",
})
Path("report.md").write_text(doc["content"], encoding="utf-8")
def print_outline(sections: list[Section], depth: int = 0) -> None:
"""Print each heading with the pages it covers."""
for section in sections:
pages = f"pages {section['startPage']}-{section['endPage']}"
print(f"{' ' * depth}{section['title']} ({pages})")
print_outline(section["children"], depth + 1)
print_outline(doc["sections"])
package main
import (
"fmt"
"log"
"os"
"strings"
)
func main() {
doc, err := CallTool[Structure]("pdf_extract_structure", map[string]any{
"input": "report.pdf",
"format": "markdown",
})
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("report.md", []byte(doc.Content), 0o644); err != nil {
log.Fatal(err)
}
printOutline(doc.Sections, 0)
}
// printOutline prints each heading with the pages it covers.
func printOutline(sections []Section, depth int) {
for _, s := range sections {
indent := strings.Repeat(" ", depth)
fmt.Printf("%s%s (pages %d-%d)\n", indent, s.Title, s.StartPage, s.EndPage)
printOutline(s.Children, depth+1)
}
}
{
"format": "markdown",
"total_pages": 4,
"has_text": true,
"truncated": false,
"content": "# Quarterly report\n\n## Summary\n\nRevenue grew 12% on the same quar…",
"sections": [
{
"title": "Quarterly report",
"level": 1,
"startPage": 1,
"id": "n1",
"endPage": 4,
"children": [
{
"title": "Summary",
"level": 2,
"startPage": 1,
"id": "n2",
"endPage": 2,
"children": []
},
{
"title": "Revenue",
"level": 2,
"startPage": 2,
"id": "n3",
"endPage": 3,
"children": []
},
{
"title": "Outlook",
"level": 2,
"startPage": 3,
"id": "n4",
"endPage": 4,
"children": []
},
{
"title": "Appendix",
"level": 2,
"startPage": 4,
"id": "n5",
"endPage": 4,
"children": []
}
]
}
]
}Text read from a PDF is whatever the document says. If you pass it to an AI model, treat it as untrusted input, not as instructions.
Handle errors
Error answers are problem details with a stable code and, for input problems, the fields to fix. Here, report.pdf has only 4 pages:
curl -i http://127.0.0.1:7341/v1/tools/pdf_extract_pages \
-H "Authorization: Bearer $SUPERPDF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "input": "report.pdf", "pages": "2-9", "output": "excerpt.pdf" }'import { ApiError, callTool, type NewFile } from './superpdf.ts';
try {
await callTool<NewFile>('pdf_extract_pages', {
input: 'report.pdf',
pages: '2-9',
output: 'excerpt.pdf',
});
} catch (error) {
if (!(error instanceof ApiError)) throw error;
const { status, code, errors = [] } = error.problem;
console.log(`${status} ${code}`);
for (const { field, message } of errors) console.log(` ${field}: ${message}`);
}
from superpdf import ApiError, call_tool
try:
call_tool("pdf_extract_pages", {
"input": "report.pdf",
"pages": "2-9",
"output": "excerpt.pdf",
})
except ApiError as error:
problem = error.problem
print(problem["status"], problem["code"])
for item in problem.get("errors", []):
print(f" {item['field']}: {item['message']}")
package main
import (
"errors"
"fmt"
"log"
)
func main() {
_, err := CallTool[NewFile]("pdf_extract_pages", map[string]any{
"input": "report.pdf",
"pages": "2-9",
"output": "excerpt.pdf",
})
var problem *Problem
if !errors.As(err, &problem) {
log.Fatal(err) // no error, or the API could not be reached
}
fmt.Println(problem.Status, problem.Code)
for _, item := range problem.Errors {
fmt.Printf(" %s: %s\n", item.Field, item.Message)
}
}
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "urn:superpdf:problem:validation",
"title": "Invalid input",
"status": 422,
"detail": "Invalid input. pages: Page ranges must be between 1 and 4.",
"instance": "urn:superpdf:request:b20a5176-152e-4b22-9438-6a5e2bc969bd",
"code": "validation",
"request_id": "b20a5176-152e-4b22-9438-6a5e2bc969bd",
"errors": [
{
"field": "pages",
"message": "Page ranges must be between 1 and 4."
}
]
}Every code is described in Errors.
Long work
A tool call answers when the tool has finished; the client files wait up to 5 minutes. For bigger work, such as a batch of 50 PDFs, run the same call as a background job instead of holding a request open.