Xyppy Print This Index source
Imported TypeScript file from server/index.ts.
Source file
server/index.ts
TypeScript205 lines
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";import cors from "cors";import express from "express";import fs from "node:fs";import path from "node:path";import { fileURLToPath } from "node:url";import { z } from "zod";import { readCheckoutRuntimeStatus } from "./commerce/runtime";import { DEFAULT_CONFIGURATION } from "../shared/catalog";import { buildCheckoutPreview } from "../shared/checkout";import type { PrintConfiguration } from "../shared/types";const SERVER_VERSION = "0.1.0";const TEMPLATE_URI = "ui://widget/xyppy-print-configurator-v1.html";const __dirname = path.dirname(fileURLToPath(import.meta.url));const DIST_HTML = path.resolve(__dirname, "..", "dist", "index.html");const fileSchema = z.object({download_url: z.string(),file_id: z.string(),mime_type: z.string().optional(),file_name: z.string().optional(),});const configurationInputSchema = {image: fileSchema.optional().describe("The image the user wants to print, when ChatGPT can resolve one from the conversation."),size: z.enum(["8x10", "11x14", "16x20", "18x24", "24x36"]).optional(),paper: z.enum(["archival-matte", "fine-art-matte"]).optional(),frame: z.enum(["unframed", "black", "white", "natural"]).optional(),mat: z.enum(["none", "white-2"]).optional(),layout: z.enum(["fill", "fit", "white-border"]).optional(),quantity: z.number().int().min(1).max(5).optional(),};function resolveConfiguration(input: Partial<PrintConfiguration>): PrintConfiguration {const configuration = {...DEFAULT_CONFIGURATION,...(input.size ? { size: input.size } : {}),...(input.paper ? { paper: input.paper } : {}),...(input.frame ? { frame: input.frame } : {}),...(input.mat ? { mat: input.mat } : {}),...(input.layout ? { layout: input.layout } : {}),...(input.quantity ? { quantity: input.quantity } : {}),};if (configuration.frame === "unframed") configuration.mat = "none";return configuration;}function readWidgetHtml(): string {if (!fs.existsSync(DIST_HTML)) {throw new Error(`Widget bundle not found at ${DIST_HTML}. Run \"pnpm build\" first.`);}return fs.readFileSync(DIST_HTML, "utf8");}export function createServer(): McpServer {const server = new McpServer({ name: "xyppy-print-this", version: SERVER_VERSION },{ instructions: "Help the user configure a prototype physical print. Never claim that a checkout, order, payment, print-ready file, or fulfillment action occurred." },);registerAppTool(server,"configure_print",{title: "Configure a Xyppy print",description: "Use this when the user wants to print an image, check suitable print sizes, or configure paper, frame, mat, crop, and quantity. This prototype estimates pricing and does not place an order.",inputSchema: configurationInputSchema,annotations: {readOnlyHint: true,destructiveHint: false,openWorldHint: false,idempotentHint: true,},_meta: {ui: { resourceUri: TEMPLATE_URI },"openai/outputTemplate": TEMPLATE_URI,"openai/fileParams": ["image"],"openai/toolInvocation/invoking": "Preparing your print options…","openai/toolInvocation/invoked": "Print options ready",},},async ({ image, size, paper, frame, mat, layout, quantity }) => {const configuration = resolveConfiguration({ size, paper, frame, mat, layout, quantity });return {content: [{type: "text" as const,text: image? "I opened the Xyppy prototype with the supplied image and estimated print options. No order has been placed.": "I opened the Xyppy prototype. Choose an image from ChatGPT or upload one to see print recommendations. No order has been placed.",}],structuredContent: {configuration,...(image ? { initialImage: { fileId: image.file_id, fileName: image.file_name, mimeType: image.mime_type } } : {}),prototypeNotice: "Prototype estimate only — no order or payment will be created.",},};},);registerAppTool(server,"preview_checkout",{title: "Preview Xyppy checkout handoff",description: "Use this when the user has configured a print and wants to review the purchase handoff. This creates a checkout preview only and never creates a cart, checkout session, order, payment, production file, or fulfillment task.",inputSchema: {...configurationInputSchema,provider: z.enum(["shopify", "stripe", "hybrid"]).optional().describe("The possible commerce backend to preview. Shopify is the default recommended path."),},annotations: {readOnlyHint: true,destructiveHint: false,openWorldHint: false,idempotentHint: true,},_meta: {ui: { resourceUri: TEMPLATE_URI },"openai/outputTemplate": TEMPLATE_URI,"openai/fileParams": ["image"],"openai/toolInvocation/invoking": "Preparing checkout preview...","openai/toolInvocation/invoked": "Checkout preview ready",},},async ({ image, size, paper, frame, mat, layout, quantity, provider }) => {const configuration = resolveConfiguration({ size, paper, frame, mat, layout, quantity });const checkoutPreview = buildCheckoutPreview(configuration, { provider, imageAttached: Boolean(image) });const checkoutRuntime = readCheckoutRuntimeStatus();return {content: [{type: "text" as const,text: "I prepared a checkout preview for this Xyppy print. No cart, checkout session, order, payment, production file, or fulfillment task was created.",}],structuredContent: {configuration,...(image ? { initialImage: { fileId: image.file_id, fileName: image.file_name, mimeType: image.mime_type } } : {}),checkoutPreview,checkoutRuntime,prototypeNotice: checkoutPreview.disclaimer,},};},);registerAppResource(server,"Xyppy Print Configurator",TEMPLATE_URI,{mimeType: RESOURCE_MIME_TYPE,description: "Interactive Xyppy print configuration widget",_meta: {ui: {prefersBorder: false,csp: { connectDomains: [], resourceDomains: [] },},"openai/widgetDescription": "An interactive print configurator for choosing an image, size, paper, frame, mat, crop treatment, and quantity, with prototype pricing.",},},async () => ({contents: [{ uri: TEMPLATE_URI, mimeType: RESOURCE_MIME_TYPE, text: readWidgetHtml() }],}),);return server;}export function createHttpApp() {const app = express();app.disable("x-powered-by");app.use(cors());app.use(express.json({ limit: "2mb" }));app.get("/health", (_req, res) => res.json({ ok: true, service: "xyppy-print-this", version: SERVER_VERSION, checkout: readCheckoutRuntimeStatus() }));app.all("/mcp", async (req, res) => {const server = createServer();const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });res.on("close", () => {void transport.close();void server.close();});try {await server.connect(transport);await transport.handleRequest(req, res, req.body);} catch (error) {console.error("MCP error:", error);if (!res.headersSent) {res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null });}}});return app;}if (process.env.NODE_ENV !== "test") {const port = Number.parseInt(process.env.PORT ?? "8000", 10);createHttpApp().listen(port, "0.0.0.0", () => {console.log(`Xyppy MCP server listening on http://localhost:${port}/mcp`);});}