@charisol/plexo-mcp 1.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.
@@ -0,0 +1,52 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ workflow_dispatch: # allows manual run from GitHub Actions UI
9
+
10
+ permissions:
11
+ contents: write # Required to push the version bump commit back to repository
12
+ id-token: write # Required for npm OIDC trusted publishing
13
+
14
+ jobs:
15
+ build-and-publish:
16
+ runs-on: ubuntu-latest
17
+
18
+ steps:
19
+ - name: Checkout source
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Set up Node.js
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: '22'
26
+ registry-url: 'https://registry.npmjs.org'
27
+
28
+ - name: Upgrade npm CLI to latest
29
+ run: npm install -g npm@latest
30
+
31
+ - name: Install dependencies
32
+ run: npm install
33
+
34
+ - name: Build MCP server package
35
+ run: npm run build
36
+
37
+ - name: Auto-bump patch version
38
+ run: |
39
+ npm version patch --no-git-tag-version
40
+
41
+ - name: Publish to npm
42
+ run: npm publish --access public
43
+ env:
44
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
45
+
46
+ - name: Commit and push version bump
47
+ run: |
48
+ git config --global user.name "github-actions[bot]"
49
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
50
+ git add package.json
51
+ git commit -m "chore: bump patch version [skip ci]"
52
+ git push
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @charisol/plexo-mcp - Plexo Model Context Protocol (MCP) Server
2
+
3
+ Official Model Context Protocol (MCP) server for **Plexo** - the AI-powered visual landing page & email template platform.
4
+
5
+ Connect **Claude Desktop**, **Claude Web**, **ChatGPT**, **Cursor**, **Windsurf**, and **Gemini** directly to your Plexo account.
6
+
7
+ ---
8
+
9
+ ## Capabilities & Tools
10
+
11
+ - 🚀 **`publish_landing_page`**: Creates, compiles, and publishes a landing page (JSON schema + HTML) to a subdomain or custom domain in 1 step.
12
+ - 📄 **`list_landing_pages`**: Retrieves saved landing page designs and active domain URLs.
13
+ - ✉️ **`list_email_templates`**: Retrieves saved email templates.
14
+ - 📊 **`get_analytics`**: Fetches total page views, unique visitor counts, and daily timelines.
15
+ - 👤 **`get_user_profile`**: Checks subscription plan, AI credit balance, and domain usage limits.
16
+ - 🗑️ **`delete_published_domain`**: Unlinks or deletes published domains.
17
+
18
+ ---
19
+
20
+ ## Quick Start & Installation
21
+
22
+ ### Option 1: Claude Desktop
23
+
24
+ Add to your `claude_desktop_config.json`:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "plexo": {
30
+ "command": "npx",
31
+ "args": ["-y", "@charisol/plexo-mcp"],
32
+ "env": {
33
+ "PLEXO_API_KEY": "plexo_sk_your_key_here",
34
+ "PLEXO_BASE_URL": "https://plexobuilder.com"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### Option 2: Cursor / Windsurf IDE
42
+
43
+ Add to `.cursor/mcp.json`:
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "plexo": {
49
+ "command": "npx",
50
+ "args": ["-y", "@charisol/plexo-mcp"],
51
+ "env": {
52
+ "PLEXO_API_KEY": "plexo_sk_your_key_here"
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### Option 3: Browser Authentication Flow
60
+ If `PLEXO_API_KEY` is omitted, the tool cleanly prompts:
61
+ `Please log into your Plexo account to authenticate: https://plexobuilder.com/mcp/login`
62
+
63
+ ---
64
+
65
+ ## Example AI Prompts
66
+
67
+ - *"Generate a modern tech SaaS landing page for an AI CRM called Acme and publish it using Plexo to subdomain acme-crm"*
68
+ - *"Show me my page view analytics for the last 7 days"*
69
+ - *"List all my email templates"*
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PlexoClient = void 0;
4
+ const config_js_1 = require("../config.js");
5
+ class PlexoClient {
6
+ baseUrl;
7
+ apiKey;
8
+ constructor(apiKey, baseUrl) {
9
+ const config = (0, config_js_1.getConfig)();
10
+ this.baseUrl = (baseUrl || config.baseUrl).replace(/\/$/, "");
11
+ this.apiKey = apiKey || config.apiKey;
12
+ }
13
+ getHeaders() {
14
+ const headers = {
15
+ "Content-Type": "application/json",
16
+ "User-Agent": "Plexo-MCP-Server/1.0",
17
+ };
18
+ if (this.apiKey) {
19
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
20
+ headers["x-api-key"] = this.apiKey;
21
+ }
22
+ return headers;
23
+ }
24
+ checkAuth() {
25
+ if (!this.apiKey) {
26
+ const loginUrl = `${this.baseUrl}/mcp/login`;
27
+ throw new Error(`Authentication Required: PLEXO_API_KEY environment variable is not configured.\n` +
28
+ `Please log into your Plexo account to authenticate: ${loginUrl}\n` +
29
+ `Once logged in, set PLEXO_API_KEY in your MCP configuration or client headers.`);
30
+ }
31
+ }
32
+ async publishLandingPage(params) {
33
+ this.checkAuth();
34
+ const res = await fetch(`${this.baseUrl}/api/v1/publish`, {
35
+ method: "POST",
36
+ headers: this.getHeaders(),
37
+ body: JSON.stringify(params),
38
+ });
39
+ const data = (await res.json());
40
+ if (!res.ok) {
41
+ throw new Error(data?.error || `Publish failed with status ${res.status}`);
42
+ }
43
+ return data;
44
+ }
45
+ async getUserProfile() {
46
+ this.checkAuth();
47
+ const res = await fetch(`${this.baseUrl}/api/v1/profile`, {
48
+ method: "GET",
49
+ headers: this.getHeaders(),
50
+ });
51
+ const data = (await res.json());
52
+ if (!res.ok) {
53
+ throw new Error(data?.error || `Failed to fetch profile (Status ${res.status})`);
54
+ }
55
+ return data;
56
+ }
57
+ async getAnalytics(templateId) {
58
+ this.checkAuth();
59
+ const url = new URL(`${this.baseUrl}/api/v1/analytics`);
60
+ if (templateId) {
61
+ url.searchParams.set("templateId", templateId);
62
+ }
63
+ const res = await fetch(url.toString(), {
64
+ method: "GET",
65
+ headers: this.getHeaders(),
66
+ });
67
+ const data = (await res.json());
68
+ if (!res.ok) {
69
+ throw new Error(data?.error || `Failed to fetch analytics (Status ${res.status})`);
70
+ }
71
+ return data;
72
+ }
73
+ async listTemplates() {
74
+ this.checkAuth();
75
+ const res = await fetch(`${this.baseUrl}/api/templates`, {
76
+ method: "GET",
77
+ headers: this.getHeaders(),
78
+ });
79
+ const data = (await res.json());
80
+ if (!res.ok) {
81
+ throw new Error(data?.error || `Failed to list templates (Status ${res.status})`);
82
+ }
83
+ return data;
84
+ }
85
+ async listDomains(templateId) {
86
+ this.checkAuth();
87
+ const url = new URL(`${this.baseUrl}/api/v1/domains`);
88
+ if (templateId) {
89
+ url.searchParams.set("templateId", templateId);
90
+ }
91
+ const res = await fetch(url.toString(), {
92
+ method: "GET",
93
+ headers: this.getHeaders(),
94
+ });
95
+ const data = (await res.json());
96
+ if (!res.ok) {
97
+ throw new Error(data?.error || `Failed to fetch domains (Status ${res.status})`);
98
+ }
99
+ return data;
100
+ }
101
+ async deleteDomain(params) {
102
+ this.checkAuth();
103
+ const url = new URL(`${this.baseUrl}/api/v1/domains`);
104
+ if (params.domainId)
105
+ url.searchParams.set("id", params.domainId);
106
+ if (params.domain)
107
+ url.searchParams.set("domain", params.domain);
108
+ const res = await fetch(url.toString(), {
109
+ method: "DELETE",
110
+ headers: this.getHeaders(),
111
+ });
112
+ const data = (await res.json());
113
+ if (!res.ok) {
114
+ throw new Error(data?.error || `Failed to delete domain (Status ${res.status})`);
115
+ }
116
+ return data;
117
+ }
118
+ }
119
+ exports.PlexoClient = PlexoClient;
package/dist/config.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getConfig = getConfig;
4
+ function getConfig() {
5
+ const baseUrl = process.env.PLEXO_BASE_URL || "https://plexobuilder.com";
6
+ const apiKey = process.env.PLEXO_API_KEY;
7
+ return {
8
+ baseUrl,
9
+ apiKey,
10
+ };
11
+ }
package/dist/index.js ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const plexoClient_js_1 = require("./client/plexoClient.js");
8
+ const server = new index_js_1.Server({
9
+ name: "plexo-mcp",
10
+ version: "1.0.0",
11
+ }, {
12
+ capabilities: {
13
+ tools: {},
14
+ },
15
+ });
16
+ // Define tool schemas
17
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
18
+ return {
19
+ tools: [
20
+ {
21
+ name: "publish_landing_page",
22
+ description: "Creates, compiles, and publishes a landing page to Plexo in one atomic step. Returns the published live URL and editable visual builder URL.",
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ name: {
27
+ type: "string",
28
+ description: "Title or name of the landing page (e.g. 'Acme AI CRM')",
29
+ },
30
+ designJson: {
31
+ type: "object",
32
+ description: "Plexo layout schema containing body style and rows array",
33
+ },
34
+ compiledHtml: {
35
+ type: "string",
36
+ description: "Optional pre-compiled HTML string. If omitted, Plexo compiles designJson automatically.",
37
+ },
38
+ domain: {
39
+ type: "string",
40
+ description: "Subdomain slug (e.g. 'acme-crm') or custom domain (e.g. 'myacme.com')",
41
+ },
42
+ type: {
43
+ type: "string",
44
+ enum: ["SUBDOMAIN", "CUSTOM"],
45
+ description: "Domain routing type (SUBDOMAIN or CUSTOM)",
46
+ },
47
+ },
48
+ required: ["designJson"],
49
+ },
50
+ },
51
+ {
52
+ name: "list_landing_pages",
53
+ description: "Lists all saved landing page templates and published domain URLs in the user's Plexo account.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {},
57
+ },
58
+ },
59
+ {
60
+ name: "list_email_templates",
61
+ description: "Lists all saved email templates in the user's Plexo account.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {},
65
+ },
66
+ },
67
+ {
68
+ name: "get_analytics",
69
+ description: "Fetches visitor analytics, total page views, unique visitor counts, and timeline data.",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ templateId: {
74
+ type: "string",
75
+ description: "Optional template ID to filter analytics for a specific page",
76
+ },
77
+ },
78
+ },
79
+ },
80
+ {
81
+ name: "get_user_profile",
82
+ description: "Retrieves user details, subscription plan (FREE, PRO, ULTRA), AI credit balance, and domain usage limits.",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {},
86
+ },
87
+ },
88
+ {
89
+ name: "delete_published_domain",
90
+ description: "Deletes or unlinks a published domain from a landing page.",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ domain: {
95
+ type: "string",
96
+ description: "Domain name to delete (e.g. 'acme.plexobuilder.com' or 'myacme.com')",
97
+ },
98
+ domainId: {
99
+ type: "string",
100
+ description: "ID of the domain record to delete",
101
+ },
102
+ },
103
+ },
104
+ },
105
+ ],
106
+ };
107
+ });
108
+ // Implement tool handlers
109
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
110
+ const { name, arguments: args } = request.params;
111
+ const client = new plexoClient_js_1.PlexoClient();
112
+ try {
113
+ switch (name) {
114
+ case "publish_landing_page": {
115
+ const result = await client.publishLandingPage(args);
116
+ return {
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: JSON.stringify(result, null, 2),
121
+ },
122
+ ],
123
+ };
124
+ }
125
+ case "list_landing_pages": {
126
+ const data = await client.listTemplates();
127
+ const landingPages = (data.templates || []).filter((t) => t.kind === "LANDING_PAGE");
128
+ const domainsRes = await client.listDomains();
129
+ return {
130
+ content: [
131
+ {
132
+ type: "text",
133
+ text: JSON.stringify({
134
+ landingPages,
135
+ publishedDomains: domainsRes.domains || [],
136
+ }, null, 2),
137
+ },
138
+ ],
139
+ };
140
+ }
141
+ case "list_email_templates": {
142
+ const data = await client.listTemplates();
143
+ const emailTemplates = (data.templates || []).filter((t) => t.kind === "EMAIL");
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: JSON.stringify({ emailTemplates }, null, 2),
149
+ },
150
+ ],
151
+ };
152
+ }
153
+ case "get_analytics": {
154
+ const result = await client.getAnalytics(args?.templateId);
155
+ return {
156
+ content: [
157
+ {
158
+ type: "text",
159
+ text: JSON.stringify(result, null, 2),
160
+ },
161
+ ],
162
+ };
163
+ }
164
+ case "get_user_profile": {
165
+ const result = await client.getUserProfile();
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: JSON.stringify(result, null, 2),
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ case "delete_published_domain": {
176
+ const result = await client.deleteDomain(args);
177
+ return {
178
+ content: [
179
+ {
180
+ type: "text",
181
+ text: JSON.stringify(result, null, 2),
182
+ },
183
+ ],
184
+ };
185
+ }
186
+ default:
187
+ throw new Error(`Unknown tool: ${name}`);
188
+ }
189
+ }
190
+ catch (error) {
191
+ return {
192
+ content: [
193
+ {
194
+ type: "text",
195
+ text: `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`,
196
+ },
197
+ ],
198
+ isError: true,
199
+ };
200
+ }
201
+ });
202
+ async function run() {
203
+ const transport = new stdio_js_1.StdioServerTransport();
204
+ await server.connect(transport);
205
+ console.error("Plexo MCP Server running on stdio transport.");
206
+ }
207
+ run().catch((err) => {
208
+ console.error("Fatal error starting Plexo MCP server:", err);
209
+ process.exit(1);
210
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@charisol/plexo-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Model Context Protocol (MCP) Server for Plexo - Landing page generation, email templates, publishing, and analytics.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "plexo-mcp": "dist/index.js"
8
+ },
9
+ "keywords": [
10
+ "plexo",
11
+ "mcp",
12
+ "model-context-protocol",
13
+ "claude",
14
+ "chatgpt",
15
+ "cursor",
16
+ "landing-page",
17
+ "email-builder"
18
+ ],
19
+ "license": "MIT",
20
+ "homepage": "https://plexo.charisol.io",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Challaris/plexo-mcp.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/Challaris/plexo-mcp/issues"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "start": "node dist/index.js",
31
+ "dev": "ts-node src/index.ts"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.1",
35
+ "zod": "^3.23.8"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.11.0",
39
+ "ts-node": "^10.9.2",
40
+ "typescript": "^5.3.3"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ }
45
+ }
@@ -0,0 +1,148 @@
1
+ import { getConfig } from "../config.js";
2
+
3
+ export class PlexoClient {
4
+ private baseUrl: string;
5
+ private apiKey?: string;
6
+
7
+ constructor(apiKey?: string, baseUrl?: string) {
8
+ const config = getConfig();
9
+ this.baseUrl = (baseUrl || config.baseUrl).replace(/\/$/, "");
10
+ this.apiKey = apiKey || config.apiKey;
11
+ }
12
+
13
+ private getHeaders(): Record<string, string> {
14
+ const headers: Record<string, string> = {
15
+ "Content-Type": "application/json",
16
+ "User-Agent": "Plexo-MCP-Server/1.0",
17
+ };
18
+
19
+ if (this.apiKey) {
20
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
21
+ headers["x-api-key"] = this.apiKey;
22
+ }
23
+
24
+ return headers;
25
+ }
26
+
27
+ private checkAuth(): void {
28
+ if (!this.apiKey) {
29
+ const loginUrl = `${this.baseUrl}/mcp/login`;
30
+ throw new Error(
31
+ `Authentication Required: PLEXO_API_KEY environment variable is not configured.\n` +
32
+ `Please log into your Plexo account to authenticate: ${loginUrl}\n` +
33
+ `Once logged in, set PLEXO_API_KEY in your MCP configuration or client headers.`
34
+ );
35
+ }
36
+ }
37
+
38
+ async publishLandingPage(params: {
39
+ name?: string;
40
+ designJson: Record<string, any>;
41
+ compiledHtml?: string;
42
+ domain?: string;
43
+ type?: "SUBDOMAIN" | "CUSTOM";
44
+ }): Promise<any> {
45
+ this.checkAuth();
46
+ const res = await fetch(`${this.baseUrl}/api/v1/publish`, {
47
+ method: "POST",
48
+ headers: this.getHeaders(),
49
+ body: JSON.stringify(params),
50
+ });
51
+
52
+ const data = (await res.json()) as any;
53
+ if (!res.ok) {
54
+ throw new Error(data?.error || `Publish failed with status ${res.status}`);
55
+ }
56
+
57
+ return data;
58
+ }
59
+
60
+ async getUserProfile(): Promise<any> {
61
+ this.checkAuth();
62
+ const res = await fetch(`${this.baseUrl}/api/v1/profile`, {
63
+ method: "GET",
64
+ headers: this.getHeaders(),
65
+ });
66
+
67
+ const data = (await res.json()) as any;
68
+ if (!res.ok) {
69
+ throw new Error(data?.error || `Failed to fetch profile (Status ${res.status})`);
70
+ }
71
+
72
+ return data;
73
+ }
74
+
75
+ async getAnalytics(templateId?: string): Promise<any> {
76
+ this.checkAuth();
77
+ const url = new URL(`${this.baseUrl}/api/v1/analytics`);
78
+ if (templateId) {
79
+ url.searchParams.set("templateId", templateId);
80
+ }
81
+
82
+ const res = await fetch(url.toString(), {
83
+ method: "GET",
84
+ headers: this.getHeaders(),
85
+ });
86
+
87
+ const data = (await res.json()) as any;
88
+ if (!res.ok) {
89
+ throw new Error(data?.error || `Failed to fetch analytics (Status ${res.status})`);
90
+ }
91
+
92
+ return data;
93
+ }
94
+
95
+ async listTemplates(): Promise<any> {
96
+ this.checkAuth();
97
+ const res = await fetch(`${this.baseUrl}/api/templates`, {
98
+ method: "GET",
99
+ headers: this.getHeaders(),
100
+ });
101
+
102
+ const data = (await res.json()) as any;
103
+ if (!res.ok) {
104
+ throw new Error(data?.error || `Failed to list templates (Status ${res.status})`);
105
+ }
106
+
107
+ return data;
108
+ }
109
+
110
+ async listDomains(templateId?: string): Promise<any> {
111
+ this.checkAuth();
112
+ const url = new URL(`${this.baseUrl}/api/v1/domains`);
113
+ if (templateId) {
114
+ url.searchParams.set("templateId", templateId);
115
+ }
116
+
117
+ const res = await fetch(url.toString(), {
118
+ method: "GET",
119
+ headers: this.getHeaders(),
120
+ });
121
+
122
+ const data = (await res.json()) as any;
123
+ if (!res.ok) {
124
+ throw new Error(data?.error || `Failed to fetch domains (Status ${res.status})`);
125
+ }
126
+
127
+ return data;
128
+ }
129
+
130
+ async deleteDomain(params: { domainId?: string; domain?: string }): Promise<any> {
131
+ this.checkAuth();
132
+ const url = new URL(`${this.baseUrl}/api/v1/domains`);
133
+ if (params.domainId) url.searchParams.set("id", params.domainId);
134
+ if (params.domain) url.searchParams.set("domain", params.domain);
135
+
136
+ const res = await fetch(url.toString(), {
137
+ method: "DELETE",
138
+ headers: this.getHeaders(),
139
+ });
140
+
141
+ const data = (await res.json()) as any;
142
+ if (!res.ok) {
143
+ throw new Error(data?.error || `Failed to delete domain (Status ${res.status})`);
144
+ }
145
+
146
+ return data;
147
+ }
148
+ }
package/src/config.ts ADDED
@@ -0,0 +1,14 @@
1
+ export interface PlexoConfig {
2
+ baseUrl: string;
3
+ apiKey?: string;
4
+ }
5
+
6
+ export function getConfig(): PlexoConfig {
7
+ const baseUrl = process.env.PLEXO_BASE_URL || "https://plexobuilder.com";
8
+ const apiKey = process.env.PLEXO_API_KEY;
9
+
10
+ return {
11
+ baseUrl,
12
+ apiKey,
13
+ };
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { PlexoClient } from "./client/plexoClient.js";
10
+
11
+ const server = new Server(
12
+ {
13
+ name: "plexo-mcp",
14
+ version: "1.0.0",
15
+ },
16
+ {
17
+ capabilities: {
18
+ tools: {},
19
+ },
20
+ }
21
+ );
22
+
23
+ // Define tool schemas
24
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
25
+ return {
26
+ tools: [
27
+ {
28
+ name: "publish_landing_page",
29
+ description:
30
+ "Creates, compiles, and publishes a landing page to Plexo in one atomic step. Returns the published live URL and editable visual builder URL.",
31
+ inputSchema: {
32
+ type: "object",
33
+ properties: {
34
+ name: {
35
+ type: "string",
36
+ description: "Title or name of the landing page (e.g. 'Acme AI CRM')",
37
+ },
38
+ designJson: {
39
+ type: "object",
40
+ description: "Plexo layout schema containing body style and rows array",
41
+ },
42
+ compiledHtml: {
43
+ type: "string",
44
+ description: "Optional pre-compiled HTML string. If omitted, Plexo compiles designJson automatically.",
45
+ },
46
+ domain: {
47
+ type: "string",
48
+ description: "Subdomain slug (e.g. 'acme-crm') or custom domain (e.g. 'myacme.com')",
49
+ },
50
+ type: {
51
+ type: "string",
52
+ enum: ["SUBDOMAIN", "CUSTOM"],
53
+ description: "Domain routing type (SUBDOMAIN or CUSTOM)",
54
+ },
55
+ },
56
+ required: ["designJson"],
57
+ },
58
+ },
59
+ {
60
+ name: "list_landing_pages",
61
+ description: "Lists all saved landing page templates and published domain URLs in the user's Plexo account.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {},
65
+ },
66
+ },
67
+ {
68
+ name: "list_email_templates",
69
+ description: "Lists all saved email templates in the user's Plexo account.",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {},
73
+ },
74
+ },
75
+ {
76
+ name: "get_analytics",
77
+ description: "Fetches visitor analytics, total page views, unique visitor counts, and timeline data.",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ templateId: {
82
+ type: "string",
83
+ description: "Optional template ID to filter analytics for a specific page",
84
+ },
85
+ },
86
+ },
87
+ },
88
+ {
89
+ name: "get_user_profile",
90
+ description: "Retrieves user details, subscription plan (FREE, PRO, ULTRA), AI credit balance, and domain usage limits.",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {},
94
+ },
95
+ },
96
+ {
97
+ name: "delete_published_domain",
98
+ description: "Deletes or unlinks a published domain from a landing page.",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: {
102
+ domain: {
103
+ type: "string",
104
+ description: "Domain name to delete (e.g. 'acme.plexobuilder.com' or 'myacme.com')",
105
+ },
106
+ domainId: {
107
+ type: "string",
108
+ description: "ID of the domain record to delete",
109
+ },
110
+ },
111
+ },
112
+ },
113
+ ],
114
+ };
115
+ });
116
+
117
+ // Implement tool handlers
118
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
119
+ const { name, arguments: args } = request.params;
120
+ const client = new PlexoClient();
121
+
122
+ try {
123
+ switch (name) {
124
+ case "publish_landing_page": {
125
+ const result = await client.publishLandingPage(args as any);
126
+ return {
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: JSON.stringify(result, null, 2),
131
+ },
132
+ ],
133
+ };
134
+ }
135
+
136
+ case "list_landing_pages": {
137
+ const data = await client.listTemplates();
138
+ const landingPages = (data.templates || []).filter(
139
+ (t: any) => t.kind === "LANDING_PAGE"
140
+ );
141
+ const domainsRes = await client.listDomains();
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: JSON.stringify(
147
+ {
148
+ landingPages,
149
+ publishedDomains: domainsRes.domains || [],
150
+ },
151
+ null,
152
+ 2
153
+ ),
154
+ },
155
+ ],
156
+ };
157
+ }
158
+
159
+ case "list_email_templates": {
160
+ const data = await client.listTemplates();
161
+ const emailTemplates = (data.templates || []).filter(
162
+ (t: any) => t.kind === "EMAIL"
163
+ );
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify({ emailTemplates }, null, 2),
169
+ },
170
+ ],
171
+ };
172
+ }
173
+
174
+ case "get_analytics": {
175
+ const result = await client.getAnalytics((args as any)?.templateId);
176
+ return {
177
+ content: [
178
+ {
179
+ type: "text",
180
+ text: JSON.stringify(result, null, 2),
181
+ },
182
+ ],
183
+ };
184
+ }
185
+
186
+ case "get_user_profile": {
187
+ const result = await client.getUserProfile();
188
+ return {
189
+ content: [
190
+ {
191
+ type: "text",
192
+ text: JSON.stringify(result, null, 2),
193
+ },
194
+ ],
195
+ };
196
+ }
197
+
198
+ case "delete_published_domain": {
199
+ const result = await client.deleteDomain(args as any);
200
+ return {
201
+ content: [
202
+ {
203
+ type: "text",
204
+ text: JSON.stringify(result, null, 2),
205
+ },
206
+ ],
207
+ };
208
+ }
209
+
210
+ default:
211
+ throw new Error(`Unknown tool: ${name}`);
212
+ }
213
+ } catch (error) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: `Error executing tool ${name}: ${
219
+ error instanceof Error ? error.message : String(error)
220
+ }`,
221
+ },
222
+ ],
223
+ isError: true,
224
+ };
225
+ }
226
+ });
227
+
228
+ async function run() {
229
+ const transport = new StdioServerTransport();
230
+ await server.connect(transport);
231
+ console.error("Plexo MCP Server running on stdio transport.");
232
+ }
233
+
234
+ run().catch((err) => {
235
+ console.error("Fatal error starting Plexo MCP server:", err);
236
+ process.exit(1);
237
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"]
15
+ }