mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 02:40:39 +09:00
Add standalone Paperclip MCP server package
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
08fea10ce1
commit
8cdba3ce18
12 changed files with 902 additions and 0 deletions
114
packages/mcp-server/src/client.ts
Normal file
114
packages/mcp-server/src/client.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import type { PaperclipMcpConfig } from "./config.js";
|
||||
|
||||
export class PaperclipApiError extends Error {
|
||||
readonly status: number;
|
||||
readonly method: string;
|
||||
readonly path: string;
|
||||
readonly body: unknown;
|
||||
|
||||
constructor(input: {
|
||||
status: number;
|
||||
method: string;
|
||||
path: string;
|
||||
body: unknown;
|
||||
message: string;
|
||||
}) {
|
||||
super(input.message);
|
||||
this.name = "PaperclipApiError";
|
||||
this.status = input.status;
|
||||
this.method = input.method;
|
||||
this.path = input.path;
|
||||
this.body = input.body;
|
||||
}
|
||||
}
|
||||
|
||||
export interface JsonRequestOptions {
|
||||
body?: unknown;
|
||||
includeRunId?: boolean;
|
||||
}
|
||||
|
||||
function isWriteMethod(method: string): boolean {
|
||||
return !["GET", "HEAD"].includes(method.toUpperCase());
|
||||
}
|
||||
|
||||
function buildErrorMessage(method: string, path: string, status: number, body: unknown): string {
|
||||
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
||||
return `${method} ${path} failed with ${status}: ${body.error}`;
|
||||
}
|
||||
return `${method} ${path} failed with ${status}`;
|
||||
}
|
||||
|
||||
async function parseResponseBody(response: Response): Promise<unknown> {
|
||||
const text = await response.text();
|
||||
if (!text) return null;
|
||||
try {
|
||||
return JSON.parse(text) as unknown;
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export class PaperclipApiClient {
|
||||
constructor(private readonly config: PaperclipMcpConfig) {}
|
||||
|
||||
get defaults() {
|
||||
return {
|
||||
companyId: this.config.companyId,
|
||||
agentId: this.config.agentId,
|
||||
runId: this.config.runId,
|
||||
};
|
||||
}
|
||||
|
||||
resolveCompanyId(companyId?: string | null): string {
|
||||
const resolved = companyId?.trim() || this.config.companyId;
|
||||
if (!resolved) {
|
||||
throw new Error("companyId is required because PAPERCLIP_COMPANY_ID is not set");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
resolveAgentId(agentId?: string | null): string {
|
||||
const resolved = agentId?.trim() || this.config.agentId;
|
||||
if (!resolved) {
|
||||
throw new Error("agentId is required because PAPERCLIP_AGENT_ID is not set");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async requestJson<T>(method: string, path: string, options: JsonRequestOptions = {}): Promise<T> {
|
||||
if (!path.startsWith("/")) {
|
||||
throw new Error(`API path must start with "/": ${path}`);
|
||||
}
|
||||
|
||||
const url = new URL(path.slice(1), `${this.config.apiUrl}/`);
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
Accept: "application/json",
|
||||
};
|
||||
if (options.body !== undefined) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
if ((options.includeRunId ?? isWriteMethod(method)) && this.config.runId) {
|
||||
headers["X-Paperclip-Run-Id"] = this.config.runId;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
});
|
||||
const parsedBody = await parseResponseBody(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new PaperclipApiError({
|
||||
status: response.status,
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
body: parsedBody,
|
||||
message: buildErrorMessage(method.toUpperCase(), path, response.status, parsedBody),
|
||||
});
|
||||
}
|
||||
|
||||
return parsedBody as T;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue