yupno-mcp-server 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,61 +1,150 @@
1
- # Yupno MCP Server
1
+ # Yupno MCP App Server
2
2
 
3
- MCP server for creating and managing events on [Yupno](https://yupno.io).
3
+ MCP App server for creating and managing events on [Yupno](https://yupno.io) with visual preview support.
4
+
5
+ ## Features
6
+
7
+ - **Visual Preview**: See your event card rendered in real-time as Claude creates it
8
+ - **Streaming Support**: Preview updates progressively as Claude generates event details
9
+ - **Theme Preview**: See colors, fonts, and background images applied immediately
10
+ - **Dual Transport**: Works with both Claude Desktop (stdio) and HTTP-based hosts
4
11
 
5
12
  ## Installation
6
13
 
14
+ ```bash
15
+ npm install yupno-mcp-server
16
+ ```
17
+
18
+ Or run directly:
19
+
7
20
  ```bash
8
21
  npx yupno-mcp-server
9
22
  ```
10
23
 
11
24
  ## Configuration
12
25
 
13
- Add to your Claude settings:
26
+ ### Claude Desktop / Claude Code (Stdio Mode)
27
+
28
+ Add to your Claude settings (`~/.claude/settings.json` or Claude Desktop config):
14
29
 
15
30
  ```json
16
31
  {
17
32
  "mcpServers": {
18
33
  "yupno": {
19
34
  "command": "npx",
20
- "args": ["-y", "yupno-mcp-server"]
35
+ "args": ["-y", "yupno-mcp-server", "--stdio"]
21
36
  }
22
37
  }
23
38
  }
24
39
  ```
25
40
 
41
+ ### HTTP Mode (for development/testing)
42
+
43
+ ```bash
44
+ npx yupno-mcp-server
45
+ # Server runs on http://localhost:3001/mcp
46
+ ```
47
+
48
+ Or with custom port:
49
+
50
+ ```bash
51
+ PORT=8080 npx yupno-mcp-server
52
+ ```
53
+
26
54
  ## Available Tools
27
55
 
28
- ### create_event
56
+ ### create_event (with UI preview)
29
57
 
30
- Create a new event with optional theming.
58
+ Create a new event with visual preview.
31
59
 
32
60
  **Example prompt:** "Create an 80s party event for tomorrow at 7pm with a neon theme"
33
61
 
62
+ **Parameters:**
63
+ - `title` (required) - Event title (2-4 words, no emojis)
64
+ - `eventDate` (required) - Start date/time in ISO 8601 format
65
+ - `location` - Physical address or venue name
66
+ - `description` - 2-3 sentences about the event
67
+ - `theme` - Visual theme options (see below)
68
+ - `customCss` - Advanced CSS customization
69
+
34
70
  ### get_event
35
71
 
36
- Get details of an existing event.
72
+ Get details of an existing event by its event token.
37
73
 
38
- ### update_event
74
+ ### update_event (with UI preview)
39
75
 
40
- Update an event (requires admin token).
76
+ Update an event (requires admin token). Shows preview of changes.
41
77
 
42
78
  ### delete_event
43
79
 
44
80
  Delete an event (requires admin token).
45
81
 
82
+ ### get_rsvps
83
+
84
+ Get the RSVP list for an event (requires admin token).
85
+
46
86
  ## Theme Options
47
87
 
48
88
  When creating events, you can specify visual themes:
49
89
 
50
- - `primaryColor` - Hex color for buttons/accents
51
- - `accentColor` - Secondary color
52
- - `backgroundColor` - Page background
53
- - `textColor` - Text color (for dark backgrounds)
54
- - `fontFamily` - Google Font name
55
- - `backgroundUrl` - HTTPS image URL
56
- - `backgroundOverlay` - Overlay opacity (0-1)
90
+ | Option | Description | Example |
91
+ |--------|-------------|---------|
92
+ | `primaryColor` | Button/accent color | `#ff6b6b` |
93
+ | `accentColor` | Secondary color | `#a84e30` |
94
+ | `backgroundColor` | Page background | `#1a1a2e` (dark) or `#faf7f2` (light) |
95
+ | `textColor` | Text color | `#ffffff` (for dark bg) or `#2d3436` (for light bg) |
96
+ | `fontFamily` | Google Font name | `Poppins`, `Inter`, `Playfair Display` |
97
+ | `backgroundUrl` | HTTPS image URL | Unsplash URL |
98
+ | `backgroundOverlay` | Overlay opacity (0-1) | `0.6` (required with backgroundUrl) |
99
+
100
+ ### Recommended Fonts
101
+
102
+ **Modern:** Poppins, Inter, Space Grotesk, Montserrat
103
+ **Elegant:** Playfair Display, Cormorant Garamond
104
+ **Bold:** Bebas Neue, Oswald, Righteous
105
+ **Fun:** Bangers, Press Start 2P (avoid Lobster, Pacifico)
106
+
107
+ ## Development
108
+
109
+ ```bash
110
+ # Install dependencies
111
+ npm install
112
+
113
+ # Run in development mode (watches for changes)
114
+ npm run dev
115
+
116
+ # Build for production
117
+ npm run build
118
+
119
+ # Test with basic-host
120
+ SERVERS='["http://localhost:3001/mcp"]' npm run basic-host
121
+ ```
122
+
123
+ ## Architecture
124
+
125
+ ```
126
+ mcp-server/
127
+ ├── server.ts # MCP server with registerAppTool/registerAppResource
128
+ ├── main.ts # Entry point with dual transport support
129
+ ├── src/
130
+ │ ├── event-preview.html # UI template
131
+ │ ├── event-preview.ts # Client-side app logic
132
+ │ ├── event-preview.css # Event card styles
133
+ │ └── global.css # Base styles
134
+ └── dist/ # Built output
135
+ ```
136
+
137
+ ## How It Works
138
+
139
+ 1. When Claude calls `create_event` or `update_event`, the server returns both:
140
+ - **Text content**: For Claude Code and non-UI hosts
141
+ - **UI reference**: Points to the event preview resource
142
+
143
+ 2. Claude Desktop fetches the UI resource and renders it in an iframe
57
144
 
58
- ### Suggested Fonts
145
+ 3. The UI receives tool input via `ontoolinput` and `ontoolinputpartial` handlers:
146
+ - `ontoolinputpartial`: Shows preview as Claude streams the event data
147
+ - `ontoolinput`: Final render when input is complete
148
+ - `ontoolresult`: Displays event links after creation
59
149
 
60
- Press Start 2P, Monoton, Righteous, Bangers, Lobster, Pacifico,
61
- Playfair Display, Oswald, Bebas Neue, Space Grotesk
150
+ 4. Theme and custom CSS are applied to the preview card in real-time
package/dist/main.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Yupno MCP App Server - Entry Point
4
+ *
5
+ * Usage:
6
+ * node dist/index.js # HTTP mode (default) - for web hosts and testing
7
+ * node dist/index.js --stdio # Stdio mode - for Claude Desktop/Claude Code
8
+ *
9
+ * HTTP mode serves on http://localhost:3001/mcp (configurable via PORT env var)
10
+ * Stdio mode communicates via stdin/stdout for CLI integration
11
+ */
12
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ /**
14
+ * Starts an MCP server with Streamable HTTP transport in stateless mode.
15
+ * This is the default mode, suitable for web-based hosts and local testing.
16
+ */
17
+ export declare function startStreamableHTTPServer(createServerFn: () => McpServer): Promise<void>;
18
+ /**
19
+ * Starts an MCP server with stdio transport.
20
+ * Use this mode for Claude Desktop and Claude Code integration.
21
+ */
22
+ export declare function startStdioServer(createServerFn: () => McpServer): Promise<void>;
package/dist/main.js ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Yupno MCP App Server - Entry Point
4
+ *
5
+ * Usage:
6
+ * node dist/index.js # HTTP mode (default) - for web hosts and testing
7
+ * node dist/index.js --stdio # Stdio mode - for Claude Desktop/Claude Code
8
+ *
9
+ * HTTP mode serves on http://localhost:3001/mcp (configurable via PORT env var)
10
+ * Stdio mode communicates via stdin/stdout for CLI integration
11
+ */
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
14
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
15
+ import cors from "cors";
16
+ import { createServer } from "./server.js";
17
+ /**
18
+ * Starts an MCP server with Streamable HTTP transport in stateless mode.
19
+ * This is the default mode, suitable for web-based hosts and local testing.
20
+ */
21
+ export async function startStreamableHTTPServer(createServerFn) {
22
+ const port = parseInt(process.env.PORT ?? "3001", 10);
23
+ const app = createMcpExpressApp({ host: "0.0.0.0" });
24
+ app.use(cors());
25
+ app.all("/mcp", async (req, res) => {
26
+ const server = createServerFn();
27
+ const transport = new StreamableHTTPServerTransport({
28
+ sessionIdGenerator: undefined,
29
+ });
30
+ res.on("close", () => {
31
+ transport.close().catch(() => { });
32
+ server.close().catch(() => { });
33
+ });
34
+ try {
35
+ await server.connect(transport);
36
+ await transport.handleRequest(req, res, req.body);
37
+ }
38
+ catch (error) {
39
+ console.error("MCP error:", error);
40
+ if (!res.headersSent) {
41
+ res.status(500).json({
42
+ jsonrpc: "2.0",
43
+ error: { code: -32603, message: "Internal server error" },
44
+ id: null,
45
+ });
46
+ }
47
+ }
48
+ });
49
+ const httpServer = app.listen(port, (err) => {
50
+ if (err) {
51
+ console.error("Failed to start server:", err);
52
+ process.exit(1);
53
+ }
54
+ console.log(`Yupno MCP App server listening on http://localhost:${port}/mcp`);
55
+ console.log("Use --stdio flag for Claude Desktop/Claude Code integration");
56
+ });
57
+ const shutdown = () => {
58
+ console.log("\nShutting down...");
59
+ httpServer.close(() => process.exit(0));
60
+ };
61
+ process.on("SIGINT", shutdown);
62
+ process.on("SIGTERM", shutdown);
63
+ }
64
+ /**
65
+ * Starts an MCP server with stdio transport.
66
+ * Use this mode for Claude Desktop and Claude Code integration.
67
+ */
68
+ export async function startStdioServer(createServerFn) {
69
+ const server = createServerFn();
70
+ const transport = new StdioServerTransport();
71
+ await server.connect(transport);
72
+ console.error("Yupno MCP App server running (stdio mode)");
73
+ }
74
+ async function main() {
75
+ if (process.argv.includes("--stdio")) {
76
+ await startStdioServer(createServer);
77
+ }
78
+ else {
79
+ await startStreamableHTTPServer(createServer);
80
+ }
81
+ }
82
+ main().catch((e) => {
83
+ console.error(e);
84
+ process.exit(1);
85
+ });
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Creates a new MCP server instance with tools and resources registered.
4
+ */
5
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Yupno MCP App Server
3
+ * Creates events with visual preview support in Claude Desktop
4
+ */
5
+ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { z } from "zod";
10
+ const YUPNO_API = process.env.YUPNO_API_URL || "https://yupno.io";
11
+ // Works both from source (server.ts) and compiled (dist/server.js)
12
+ const DIST_DIR = import.meta.filename.endsWith(".ts")
13
+ ? path.join(import.meta.dirname, "dist")
14
+ : import.meta.dirname;
15
+ // Resource URI for the event preview UI
16
+ const EVENT_PREVIEW_RESOURCE = "ui://yupno/event-preview.html";
17
+ // Shared theme schema
18
+ const themeSchema = z.object({
19
+ primaryColor: z.string().optional().describe("Hex color for buttons"),
20
+ accentColor: z.string().optional().describe("Hex color for secondary elements"),
21
+ backgroundColor: z.string().optional().describe("Hex color for page background"),
22
+ textColor: z.string().optional().describe("Hex color for text"),
23
+ fontFamily: z.string().optional().describe("Google Font name"),
24
+ backgroundUrl: z.string().optional().describe("HTTPS URL for background image"),
25
+ backgroundOverlay: z.number().min(0).max(1).optional().describe("Dark overlay opacity 0-1"),
26
+ }).optional();
27
+ /**
28
+ * Creates a new MCP server instance with tools and resources registered.
29
+ */
30
+ export function createServer() {
31
+ const server = new McpServer({
32
+ name: "Yupno Events",
33
+ version: "2.0.0",
34
+ });
35
+ // Register the event preview UI resource
36
+ registerAppResource(server, EVENT_PREVIEW_RESOURCE, EVENT_PREVIEW_RESOURCE, { mimeType: RESOURCE_MIME_TYPE }, async () => {
37
+ const html = await fs.readFile(path.join(DIST_DIR, "src", "event-preview.html"), "utf-8");
38
+ return {
39
+ contents: [
40
+ { uri: EVENT_PREVIEW_RESOURCE, mimeType: RESOURCE_MIME_TYPE, text: html },
41
+ ],
42
+ };
43
+ });
44
+ // Create event tool with UI support
45
+ registerAppTool(server, "create_event", {
46
+ title: "Create Event",
47
+ description: `Create a new event on Yupno. Returns admin URL (for managing) and guest URL (for sharing).
48
+
49
+ IMPORTANT - ALWAYS INCLUDE:
50
+ - title: Short (2-4 words), NO emojis
51
+ - eventDate: When the event starts
52
+ - location: Where it's happening
53
+ - description: 2-3 sentences about the event (what to expect, dress code, etc.)
54
+
55
+ DESIGN RULES:
56
+ - Fonts: Use Poppins, Inter, Playfair Display, or Montserrat. AVOID Lobster/Pacifico
57
+ - Colors: Dark bg (#1a1a2e) needs light text (#ffffff). Light bg (#faf7f2) needs dark text (#2d3436)
58
+ - Background images: Always set backgroundOverlay 0.5-0.7 for readability
59
+
60
+ EXAMPLE:
61
+ {
62
+ "title": "Summer Beats",
63
+ "eventDate": "2024-06-15T20:00:00",
64
+ "location": "The Rooftop, 123 Main St",
65
+ "description": "Join us for an unforgettable night of music and dancing. Dress code: smart casual.",
66
+ "theme": { "primaryColor": "#ff6b6b", "backgroundColor": "#1a1a2e", "textColor": "#ffffff", "fontFamily": "Poppins" }
67
+ }`,
68
+ inputSchema: {
69
+ title: z.string().describe("Event title - keep SHORT (2-4 words), NO emojis"),
70
+ eventDate: z.string().describe("Start date/time in ISO 8601 format (REQUIRED)"),
71
+ endDate: z.string().optional().describe("End date/time in ISO 8601 format"),
72
+ timezone: z.string().optional().describe("IANA timezone (e.g., America/New_York)"),
73
+ location: z.string().optional().describe("Physical address or venue name"),
74
+ description: z.string().optional().describe("2-3 sentences about the event"),
75
+ organizerName: z.string().optional().describe("Name of the host/organizer"),
76
+ showGuestList: z.boolean().optional().describe("Show who's attending"),
77
+ collectEmail: z.boolean().optional().describe("Ask for guest email"),
78
+ rsvpDeadline: z.string().optional().describe("ISO 8601 date/time for RSVP cutoff"),
79
+ rsvpDeadlineHard: z.boolean().optional().describe("If true, block RSVPs after deadline"),
80
+ theme: themeSchema.describe("Visual theme"),
81
+ customCss: z.string().optional().describe("Raw CSS to inject"),
82
+ },
83
+ _meta: { ui: { resourceUri: EVENT_PREVIEW_RESOURCE } },
84
+ }, async (args) => {
85
+ try {
86
+ const res = await fetch(`${YUPNO_API}/api/v1/events`, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify(args),
90
+ });
91
+ const data = (await res.json());
92
+ if (!res.ok) {
93
+ return {
94
+ content: [{ type: "text", text: `Error: ${data.error || JSON.stringify(data)}` }],
95
+ structuredContent: { error: data.error || "Failed to create event" },
96
+ };
97
+ }
98
+ return {
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text: `Event created successfully!
103
+
104
+ Admin URL (save this - for managing RSVPs): ${data.links.admin}
105
+ Guest URL (share this): ${data.links.guest}
106
+
107
+ Admin Token: ${data.adminToken}
108
+ Event Token: ${data.eventToken}`,
109
+ },
110
+ ],
111
+ structuredContent: {
112
+ links: data.links,
113
+ adminToken: data.adminToken,
114
+ eventToken: data.eventToken,
115
+ },
116
+ };
117
+ }
118
+ catch (error) {
119
+ const message = error instanceof Error ? error.message : "Unknown error";
120
+ return {
121
+ content: [{ type: "text", text: `Error: ${message}` }],
122
+ structuredContent: { error: message },
123
+ };
124
+ }
125
+ });
126
+ // Get event tool (no UI needed - just returns data)
127
+ server.tool("get_event", "Get public details of an event by its event token.", {
128
+ eventToken: z.string().describe("The event token from the guest URL"),
129
+ }, async ({ eventToken }) => {
130
+ try {
131
+ const res = await fetch(`${YUPNO_API}/api/v1/events/${eventToken}`);
132
+ const data = (await res.json());
133
+ if (!res.ok) {
134
+ return { content: [{ type: "text", text: `Error: ${data.error || "Event not found"}` }] };
135
+ }
136
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : "Unknown error";
140
+ return { content: [{ type: "text", text: `Error: ${message}` }] };
141
+ }
142
+ });
143
+ // Update event tool with UI support
144
+ registerAppTool(server, "update_event", {
145
+ title: "Update Event",
146
+ description: "Update an existing event. Requires the admin token.",
147
+ inputSchema: {
148
+ adminToken: z.string().describe("The admin token for the event"),
149
+ title: z.string().optional(),
150
+ eventDate: z.string().optional(),
151
+ endDate: z.string().optional(),
152
+ timezone: z.string().optional(),
153
+ location: z.string().optional(),
154
+ description: z.string().optional(),
155
+ rsvpDeadline: z.string().optional().describe("ISO 8601 date/time for RSVP cutoff"),
156
+ rsvpDeadlineHard: z.boolean().optional().describe("If true, block RSVPs after deadline"),
157
+ theme: themeSchema,
158
+ customCss: z.string().optional().describe("Raw CSS to inject into event page"),
159
+ },
160
+ _meta: { ui: { resourceUri: EVENT_PREVIEW_RESOURCE } },
161
+ }, async ({ adminToken, ...updateData }) => {
162
+ try {
163
+ const res = await fetch(`${YUPNO_API}/api/v1/events/${adminToken}`, {
164
+ method: "PUT",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify(updateData),
167
+ });
168
+ const data = (await res.json());
169
+ if (!res.ok) {
170
+ return {
171
+ content: [{ type: "text", text: `Error: ${data.error || JSON.stringify(data)}` }],
172
+ structuredContent: { success: false, error: data.error },
173
+ };
174
+ }
175
+ return {
176
+ content: [{ type: "text", text: "Event updated successfully!" }],
177
+ structuredContent: { success: true },
178
+ };
179
+ }
180
+ catch (error) {
181
+ const message = error instanceof Error ? error.message : "Unknown error";
182
+ return {
183
+ content: [{ type: "text", text: `Error: ${message}` }],
184
+ structuredContent: { success: false, error: message },
185
+ };
186
+ }
187
+ });
188
+ // Delete event tool
189
+ server.tool("delete_event", "Delete an event. Requires the admin token. This action cannot be undone.", {
190
+ adminToken: z.string().describe("The admin token for the event"),
191
+ }, async ({ adminToken }) => {
192
+ try {
193
+ const res = await fetch(`${YUPNO_API}/api/v1/events/${adminToken}`, {
194
+ method: "DELETE",
195
+ });
196
+ if (!res.ok) {
197
+ const data = (await res.json());
198
+ return { content: [{ type: "text", text: `Error: ${data.error || "Failed to delete"}` }] };
199
+ }
200
+ return { content: [{ type: "text", text: "Event deleted successfully." }] };
201
+ }
202
+ catch (error) {
203
+ const message = error instanceof Error ? error.message : "Unknown error";
204
+ return { content: [{ type: "text", text: `Error: ${message}` }] };
205
+ }
206
+ });
207
+ // Get RSVPs tool
208
+ server.tool("get_rsvps", "Get the list of RSVPs for an event. Requires both the event token and admin token.", {
209
+ eventToken: z.string().describe("The event token (16 hex chars from guest URL)"),
210
+ adminToken: z.string().describe("The admin token for the event"),
211
+ }, async ({ eventToken, adminToken }) => {
212
+ try {
213
+ const res = await fetch(`${YUPNO_API}/api/v1/events/${eventToken}/rsvps`, {
214
+ headers: {
215
+ Authorization: `Bearer ${adminToken}`,
216
+ },
217
+ });
218
+ const data = (await res.json());
219
+ if (!res.ok) {
220
+ return { content: [{ type: "text", text: `Error: ${data.error || "Failed to get RSVPs"}` }] };
221
+ }
222
+ const { summary, rsvps } = data;
223
+ let text = `RSVPs Summary:
224
+ - Total responses: ${summary.total}
225
+ - Attending: ${summary.attending}
226
+ - Declined: ${summary.declined}
227
+ - Total guests (with +1s): ${summary.totalGuests}
228
+ - Plus ones: ${summary.plusOnes}
229
+
230
+ `;
231
+ if (rsvps.length === 0) {
232
+ text += "No RSVPs yet.";
233
+ }
234
+ else {
235
+ text += "Guest List:\n";
236
+ for (const rsvp of rsvps) {
237
+ const status = rsvp.attending ? "Yes" : "No";
238
+ const plusOne = rsvp.plusOne ? " (+1)" : "";
239
+ text += `- ${rsvp.name}${plusOne}: ${status}`;
240
+ if (rsvp.email)
241
+ text += ` | ${rsvp.email}`;
242
+ text += "\n";
243
+ }
244
+ }
245
+ return { content: [{ type: "text", text }] };
246
+ }
247
+ catch (error) {
248
+ const message = error instanceof Error ? error.message : "Unknown error";
249
+ return { content: [{ type: "text", text: `Error: ${message}` }] };
250
+ }
251
+ });
252
+ return server;
253
+ }