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 +108 -19
- package/dist/main.d.ts +22 -0
- package/dist/main.js +85 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +253 -0
- package/dist/src/event-preview.html +166 -0
- package/package.json +36 -7
- package/index.js +0 -250
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
|
-
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/dist/server.d.ts
ADDED
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
|
+
}
|