lovieme-mcp 0.0.0-stage → 0.1.1
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 +103 -3
- package/bin/lovieme-mcp.js +9 -0
- package/dist/client.d.ts +67 -0
- package/dist/client.js +151 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +34 -0
- package/dist/tools.d.ts +10 -0
- package/dist/tools.js +359 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/package.json +43 -6
package/README.md
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# 🤖 LovieMe MCP Server (`lovieme-mcp`)
|
|
2
|
+
|
|
3
|
+
The official **Model Context Protocol (MCP)** server for [LovieMe](https://www.lovie.me) — enabling AI coding agents (Cursor, Claude Code, Antigravity, Windsurf, Devin) to autonomously check availability, claim subdomains, configure Anycast DNS records, and apply 1-click cloud hosting presets directly from chat.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⚡ Quickstart
|
|
8
|
+
|
|
9
|
+
### Run with `npx` (Zero Installation)
|
|
10
|
+
```bash
|
|
11
|
+
npx lovieme-mcp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🔑 Authentication
|
|
17
|
+
|
|
18
|
+
Set your LovieMe API key as an environment variable:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export LOVIE_API_KEY="lovie_live_your_api_key_here"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> **Tip:** You can generate a dedicated AI Agent API token in your [LovieMe Dashboard](https://www.lovie.me/dashboard/api-keys) with granular least-privilege permissions (`subdomains:read/write`, `dns:read/write`, `profile:read`).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🛠️ Configuration for AI Agents
|
|
29
|
+
|
|
30
|
+
### 1. Cursor (`.cursor/mcp.json`)
|
|
31
|
+
Add this to your project or global Cursor MCP configuration:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"lovieme": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "lovieme-mcp"],
|
|
39
|
+
"env": {
|
|
40
|
+
"LOVIE_API_KEY": "lovie_live_your_key_here"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Claude Code (`claude.json`)
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"lovieme": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "lovieme-mcp"],
|
|
54
|
+
"env": {
|
|
55
|
+
"LOVIE_API_KEY": "lovie_live_your_key_here"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Claude Desktop App (`claude_desktop_config.json`)
|
|
63
|
+
* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
64
|
+
* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"lovieme": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "lovieme-mcp"],
|
|
72
|
+
"env": {
|
|
73
|
+
"LOVIE_API_KEY": "lovie_live_your_key_here"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 📋 Available MCP Tools
|
|
83
|
+
|
|
84
|
+
| Tool | Description | Required Scopes |
|
|
85
|
+
| :--- | :--- | :---: |
|
|
86
|
+
| `lovie_check_subdomain` | Check if a `*.lovie.me` prefix is available | Public (None) |
|
|
87
|
+
| `lovie_claim_subdomain` | Claim a new subdomain under your account | `subdomains:write` |
|
|
88
|
+
| `lovie_list_subdomains` | List all owned subdomains and DNS record counts | `subdomains:read` |
|
|
89
|
+
| `lovie_get_subdomain` | Inspect details and active DNS records of a subdomain | `subdomains:read` |
|
|
90
|
+
| `lovie_delete_subdomain`| Release a subdomain and purge edge records | `subdomains:write` |
|
|
91
|
+
| `lovie_list_dns_records`| List active DNS records for a given subdomain | `dns:read` |
|
|
92
|
+
| `lovie_create_dns_record`| Create A, AAAA, CNAME, TXT, or MX records | `dns:write` |
|
|
93
|
+
| `lovie_delete_dns_record`| Remove an existing DNS record by ID | `dns:write` |
|
|
94
|
+
| `lovie_list_presets` | List 1-click presets (Vercel, Railway, Render, etc.) | Public (None) |
|
|
95
|
+
| `lovie_apply_preset` | Apply a 1-click cloud hosting preset to a subdomain | `dns:write` |
|
|
96
|
+
| `lovie_verify_doh` | Test propagation across Google, Cloudflare, and Quad9 | Public (None) |
|
|
97
|
+
| `lovie_get_profile` | Check account tier and remaining subdomain quota | `profile:read` |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 📄 License
|
|
102
|
+
MIT © Sagar Kewat (Sagarithm)
|
|
103
|
+
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { LovieMeProfile, LovieMeSubdomain, LovieMeDnsRecord, CloudPreset, DoHResolutionResult } from "./types.js";
|
|
2
|
+
export declare class LovieMeClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private apiKey;
|
|
5
|
+
constructor(apiKey?: string, baseUrl?: string);
|
|
6
|
+
setApiKey(key: string): void;
|
|
7
|
+
hasApiKey(): boolean;
|
|
8
|
+
private request;
|
|
9
|
+
checkSubdomain(name: string): Promise<{
|
|
10
|
+
available: boolean;
|
|
11
|
+
name: string;
|
|
12
|
+
fqdn: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}>;
|
|
15
|
+
getProfile(): Promise<{
|
|
16
|
+
profile: LovieMeProfile;
|
|
17
|
+
}>;
|
|
18
|
+
listSubdomains(): Promise<{
|
|
19
|
+
subdomains: LovieMeSubdomain[];
|
|
20
|
+
}>;
|
|
21
|
+
getSubdomain(name: string): Promise<{
|
|
22
|
+
subdomain: LovieMeSubdomain;
|
|
23
|
+
dns_records: LovieMeDnsRecord[];
|
|
24
|
+
}>;
|
|
25
|
+
claimSubdomain(name: string): Promise<{
|
|
26
|
+
success: boolean;
|
|
27
|
+
message: string;
|
|
28
|
+
subdomain: LovieMeSubdomain;
|
|
29
|
+
}>;
|
|
30
|
+
deleteSubdomain(name: string): Promise<{
|
|
31
|
+
success: boolean;
|
|
32
|
+
message: string;
|
|
33
|
+
}>;
|
|
34
|
+
listDnsRecords(subdomain: string): Promise<{
|
|
35
|
+
dns_records: LovieMeDnsRecord[];
|
|
36
|
+
}>;
|
|
37
|
+
createDnsRecord(data: {
|
|
38
|
+
subdomain: string;
|
|
39
|
+
type: "A" | "AAAA" | "CNAME" | "TXT" | "MX";
|
|
40
|
+
name: string;
|
|
41
|
+
content: string;
|
|
42
|
+
ttl?: number;
|
|
43
|
+
priority?: number;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
success: boolean;
|
|
46
|
+
message: string;
|
|
47
|
+
record: LovieMeDnsRecord;
|
|
48
|
+
}>;
|
|
49
|
+
deleteDnsRecord(id: string): Promise<{
|
|
50
|
+
success: boolean;
|
|
51
|
+
message: string;
|
|
52
|
+
}>;
|
|
53
|
+
listPresets(): Promise<{
|
|
54
|
+
presets: CloudPreset[];
|
|
55
|
+
}>;
|
|
56
|
+
applyPreset(data: {
|
|
57
|
+
subdomain: string;
|
|
58
|
+
preset: string;
|
|
59
|
+
customTarget?: string;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
success: boolean;
|
|
62
|
+
message: string;
|
|
63
|
+
created_records: LovieMeDnsRecord[];
|
|
64
|
+
}>;
|
|
65
|
+
verifyDoH(domain: string, type?: string): Promise<DoHResolutionResult[]>;
|
|
66
|
+
private ensureAuth;
|
|
67
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import dns from "node:dns";
|
|
2
|
+
try {
|
|
3
|
+
dns.setDefaultResultOrder?.("ipv4first");
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
// Ignore in environments where setDefaultResultOrder is unsupported
|
|
7
|
+
}
|
|
8
|
+
export class LovieMeClient {
|
|
9
|
+
baseUrl;
|
|
10
|
+
apiKey;
|
|
11
|
+
constructor(apiKey, baseUrl) {
|
|
12
|
+
this.apiKey = apiKey || process.env.LOVIE_API_KEY || "";
|
|
13
|
+
this.baseUrl = (baseUrl || process.env.LOVIE_API_URL || "https://www.lovie.me").replace(/\/+$/, "");
|
|
14
|
+
}
|
|
15
|
+
setApiKey(key) {
|
|
16
|
+
this.apiKey = key;
|
|
17
|
+
}
|
|
18
|
+
hasApiKey() {
|
|
19
|
+
return Boolean(this.apiKey && this.apiKey.trim().length > 0);
|
|
20
|
+
}
|
|
21
|
+
async request(path, options = {}) {
|
|
22
|
+
const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
23
|
+
const headers = {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"User-Agent": "LovieMe-MCP-Server/0.1.1",
|
|
26
|
+
...(options.headers || {}),
|
|
27
|
+
};
|
|
28
|
+
if (this.apiKey) {
|
|
29
|
+
headers["Authorization"] = `Bearer ${this.apiKey.trim()}`;
|
|
30
|
+
}
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
...options,
|
|
33
|
+
headers,
|
|
34
|
+
});
|
|
35
|
+
const contentType = response.headers.get("content-type") || "";
|
|
36
|
+
const isJson = contentType.includes("application/json");
|
|
37
|
+
const payload = isJson ? await response.json() : await response.text();
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const errorMsg = (typeof payload === "object" && payload !== null && (payload.error || payload.message)) ||
|
|
40
|
+
`HTTP ${response.status}: ${response.statusText}`;
|
|
41
|
+
throw new Error(String(errorMsg));
|
|
42
|
+
}
|
|
43
|
+
return payload;
|
|
44
|
+
}
|
|
45
|
+
// 1. Availability check (public endpoint, works without API key)
|
|
46
|
+
async checkSubdomain(name) {
|
|
47
|
+
const cleanName = name.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
48
|
+
return this.request(`/api/v1/subdomains/check?name=${encodeURIComponent(cleanName)}`);
|
|
49
|
+
}
|
|
50
|
+
// 2. Profile & quota
|
|
51
|
+
async getProfile() {
|
|
52
|
+
this.ensureAuth();
|
|
53
|
+
return this.request("/api/v1/me");
|
|
54
|
+
}
|
|
55
|
+
// 3. Subdomains
|
|
56
|
+
async listSubdomains() {
|
|
57
|
+
this.ensureAuth();
|
|
58
|
+
return this.request("/api/v1/subdomains");
|
|
59
|
+
}
|
|
60
|
+
async getSubdomain(name) {
|
|
61
|
+
this.ensureAuth();
|
|
62
|
+
const cleanName = name.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
63
|
+
return this.request(`/api/v1/subdomains/${encodeURIComponent(cleanName)}`);
|
|
64
|
+
}
|
|
65
|
+
async claimSubdomain(name) {
|
|
66
|
+
this.ensureAuth();
|
|
67
|
+
const cleanName = name.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
68
|
+
return this.request("/api/v1/subdomains", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify({ name: cleanName }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async deleteSubdomain(name) {
|
|
74
|
+
this.ensureAuth();
|
|
75
|
+
const cleanName = name.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
76
|
+
return this.request(`/api/v1/subdomains/${encodeURIComponent(cleanName)}`, {
|
|
77
|
+
method: "DELETE",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// 4. DNS Records
|
|
81
|
+
async listDnsRecords(subdomain) {
|
|
82
|
+
this.ensureAuth();
|
|
83
|
+
const cleanSub = subdomain.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
84
|
+
return this.request(`/api/v1/dns/records?subdomain=${encodeURIComponent(cleanSub)}`);
|
|
85
|
+
}
|
|
86
|
+
async createDnsRecord(data) {
|
|
87
|
+
this.ensureAuth();
|
|
88
|
+
return this.request("/api/v1/dns/records", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: JSON.stringify(data),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async deleteDnsRecord(id) {
|
|
94
|
+
this.ensureAuth();
|
|
95
|
+
return this.request(`/api/v1/dns/records/${encodeURIComponent(id)}`, {
|
|
96
|
+
method: "DELETE",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// 5. Presets
|
|
100
|
+
async listPresets() {
|
|
101
|
+
return this.request("/api/v1/dns/presets");
|
|
102
|
+
}
|
|
103
|
+
async applyPreset(data) {
|
|
104
|
+
this.ensureAuth();
|
|
105
|
+
return this.request("/api/v1/dns/presets", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: JSON.stringify(data),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// 6. Global DoH Multi-Resolver Verification
|
|
111
|
+
async verifyDoH(domain, type = "A") {
|
|
112
|
+
const clean = domain.trim().toLowerCase();
|
|
113
|
+
const fqdn = clean === "lovie.me" || clean.endsWith(".lovie.me") ? clean : `${clean}.lovie.me`;
|
|
114
|
+
const resolvers = [
|
|
115
|
+
{ name: "Google Public DNS", url: `https://dns.google/resolve?name=${encodeURIComponent(fqdn)}&type=${type}` },
|
|
116
|
+
{ name: "Cloudflare (1.1.1.1)", url: `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(fqdn)}&type=${type}` },
|
|
117
|
+
{ name: "AliDNS Global", url: `https://dns.alidns.com/resolve?name=${encodeURIComponent(fqdn)}&type=${type}` },
|
|
118
|
+
];
|
|
119
|
+
const results = await Promise.allSettled(resolvers.map(async ({ name, url }) => {
|
|
120
|
+
const start = Date.now();
|
|
121
|
+
const res = await fetch(url, {
|
|
122
|
+
headers: { Accept: "application/dns-json" },
|
|
123
|
+
});
|
|
124
|
+
const latency = Date.now() - start;
|
|
125
|
+
if (!res.ok)
|
|
126
|
+
throw new Error(`HTTP ${res.status}`);
|
|
127
|
+
const data = (await res.json());
|
|
128
|
+
return {
|
|
129
|
+
resolver: name,
|
|
130
|
+
status: (data.Answer && data.Answer.length > 0 ? "resolved" : "not_found"),
|
|
131
|
+
latency_ms: latency,
|
|
132
|
+
answers: data.Answer || [],
|
|
133
|
+
};
|
|
134
|
+
}));
|
|
135
|
+
return results.map((r, i) => {
|
|
136
|
+
if (r.status === "fulfilled")
|
|
137
|
+
return r.value;
|
|
138
|
+
return {
|
|
139
|
+
resolver: resolvers[i].name,
|
|
140
|
+
status: "error",
|
|
141
|
+
latency_ms: 0,
|
|
142
|
+
answers: [],
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
ensureAuth() {
|
|
147
|
+
if (!this.apiKey) {
|
|
148
|
+
throw new Error("LovieMe API key required. Set the LOVIE_API_KEY environment variable or pass an API key (format: lovie_live_...).");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { LovieMeClient } from "./client.js";
|
|
5
|
+
import { LOVIE_TOOLS, handleToolCall } from "./tools.js";
|
|
6
|
+
export function createServer(apiKey, baseUrl) {
|
|
7
|
+
const client = new LovieMeClient(apiKey, baseUrl);
|
|
8
|
+
const server = new Server({
|
|
9
|
+
name: "lovieme-mcp",
|
|
10
|
+
version: "0.1.0",
|
|
11
|
+
}, {
|
|
12
|
+
capabilities: {
|
|
13
|
+
tools: {},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
// 1. List available tools
|
|
17
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
18
|
+
return {
|
|
19
|
+
tools: LOVIE_TOOLS,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
// 2. Call tool
|
|
23
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
24
|
+
const { name, arguments: args } = request.params;
|
|
25
|
+
return handleToolCall(name, args, client);
|
|
26
|
+
});
|
|
27
|
+
return server;
|
|
28
|
+
}
|
|
29
|
+
export async function main() {
|
|
30
|
+
const server = createServer();
|
|
31
|
+
const transport = new StdioServerTransport();
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
console.error("LovieMe MCP Server running on stdio");
|
|
34
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { LovieMeClient } from "./client.js";
|
|
3
|
+
export declare const LOVIE_TOOLS: Tool[];
|
|
4
|
+
export declare function handleToolCall(name: string, args: Record<string, unknown> | undefined, client: LovieMeClient): Promise<{
|
|
5
|
+
content: Array<{
|
|
6
|
+
type: "text";
|
|
7
|
+
text: string;
|
|
8
|
+
}>;
|
|
9
|
+
isError?: boolean;
|
|
10
|
+
}>;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
export const LOVIE_TOOLS = [
|
|
2
|
+
{
|
|
3
|
+
name: "lovie_check_subdomain",
|
|
4
|
+
description: "Check whether a *.lovie.me subdomain prefix is available for registration.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
name: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "Subdomain prefix to check (e.g. 'my-app' for my-app.lovie.me).",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
required: ["name"],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "lovie_claim_subdomain",
|
|
18
|
+
description: "Claim and register a new *.lovie.me subdomain. Requires LOVIE_API_KEY with 'subdomains:write' scope.",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
name: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Subdomain prefix to claim (alphanumeric and hyphens, 3-63 chars).",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ["name"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "lovie_list_subdomains",
|
|
32
|
+
description: "List all subdomains owned by the authenticated account with their DNS records count. Requires LOVIE_API_KEY with 'subdomains:read' scope.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "lovie_get_subdomain",
|
|
40
|
+
description: "Inspect details of a specific subdomain including its active DNS records. Requires LOVIE_API_KEY with 'subdomains:read' scope.",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
name: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Subdomain name or prefix to inspect.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
required: ["name"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "lovie_delete_subdomain",
|
|
54
|
+
description: "Release and permanently delete a subdomain, purging all its DNS records from Gcore Anycast nameservers. Requires LOVIE_API_KEY with 'subdomains:write' scope.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
name: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Subdomain prefix to release (e.g. 'test-app').",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ["name"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "lovie_list_dns_records",
|
|
68
|
+
description: "List all active DNS records for a given subdomain. Requires LOVIE_API_KEY with 'dns:read' scope.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
subdomain: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Subdomain prefix (e.g. 'my-app').",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["subdomain"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "lovie_create_dns_record",
|
|
82
|
+
description: "Create an RFC-compliant DNS record (A, AAAA, CNAME, TXT, MX) with sub-2s Gcore Anycast propagation. Requires LOVIE_API_KEY with 'dns:write' scope.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
subdomain: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Subdomain prefix where the record belongs (e.g. 'my-app').",
|
|
89
|
+
},
|
|
90
|
+
type: {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: ["A", "AAAA", "CNAME", "TXT", "MX"],
|
|
93
|
+
description: "DNS record type.",
|
|
94
|
+
},
|
|
95
|
+
name: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Host label ('@' for apex of subdomain, or 'www', 'api', etc.).",
|
|
98
|
+
},
|
|
99
|
+
content: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Record destination (IPv4 for A, IPv6 for AAAA, target host for CNAME, string for TXT).",
|
|
102
|
+
},
|
|
103
|
+
ttl: {
|
|
104
|
+
type: "number",
|
|
105
|
+
description: "TTL in seconds (default: 300).",
|
|
106
|
+
},
|
|
107
|
+
priority: {
|
|
108
|
+
type: "number",
|
|
109
|
+
description: "MX record priority (required for MX, e.g. 10).",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["subdomain", "type", "name", "content"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "lovie_delete_dns_record",
|
|
117
|
+
description: "Delete an existing DNS record by ID, removing it from both database and Anycast edge. Requires LOVIE_API_KEY with 'dns:write' scope.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
id: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "The unique UUID of the DNS record to delete.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
required: ["id"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "lovie_list_presets",
|
|
131
|
+
description: "List all verified 1-click cloud hosting presets (Vercel, Railway, Render, GitHub Pages, Cloudflare Pages, Netlify, Fly.io).",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "lovie_apply_preset",
|
|
139
|
+
description: "Apply a 1-click cloud hosting DNS preset to your subdomain. Automatically configures tested CNAME and A records. Requires LOVIE_API_KEY with 'dns:write' scope.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
subdomain: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Subdomain prefix to apply preset to (e.g. 'my-app').",
|
|
146
|
+
},
|
|
147
|
+
preset: {
|
|
148
|
+
type: "string",
|
|
149
|
+
enum: ["vercel", "railway", "render", "github-pages", "cloudflare-pages", "netlify", "flyio"],
|
|
150
|
+
description: "Target platform preset ID.",
|
|
151
|
+
},
|
|
152
|
+
customTarget: {
|
|
153
|
+
type: "string",
|
|
154
|
+
description: "Optional custom target required for some presets (e.g. GitHub username for github-pages, or Railway target domain).",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ["subdomain", "preset"],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "lovie_verify_doh",
|
|
162
|
+
description: "Verify live Anycast DNS propagation worldwide by parallel-querying Google (8.8.8.8), Cloudflare (1.1.1.1), and Quad9 (9.9.9.9) DoH resolvers.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
domain: {
|
|
167
|
+
type: "string",
|
|
168
|
+
description: "Subdomain to test (e.g. 'my-app' or 'my-app.lovie.me').",
|
|
169
|
+
},
|
|
170
|
+
type: {
|
|
171
|
+
type: "string",
|
|
172
|
+
enum: ["A", "AAAA", "CNAME", "TXT", "MX"],
|
|
173
|
+
description: "Record type to verify (default: 'A').",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
required: ["domain"],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "lovie_get_profile",
|
|
181
|
+
description: "Inspect the current user profile, plan tier (Free / Pro Lifetime / Builder Grant), and remaining subdomain quotas. Requires LOVIE_API_KEY with 'profile:read' scope.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
export async function handleToolCall(name, args, client) {
|
|
189
|
+
try {
|
|
190
|
+
const p = args || {};
|
|
191
|
+
switch (name) {
|
|
192
|
+
case "lovie_check_subdomain": {
|
|
193
|
+
const result = await client.checkSubdomain(String(p.name));
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: JSON.stringify(result, null, 2),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
case "lovie_claim_subdomain": {
|
|
204
|
+
const result = await client.claimSubdomain(String(p.name));
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: JSON.stringify(result, null, 2),
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
case "lovie_list_subdomains": {
|
|
215
|
+
const result = await client.listSubdomains();
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: JSON.stringify(result, null, 2),
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
case "lovie_get_subdomain": {
|
|
226
|
+
const result = await client.getSubdomain(String(p.name));
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: JSON.stringify(result, null, 2),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
case "lovie_delete_subdomain": {
|
|
237
|
+
const result = await client.deleteSubdomain(String(p.name));
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: "text",
|
|
242
|
+
text: JSON.stringify(result, null, 2),
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
case "lovie_list_dns_records": {
|
|
248
|
+
const result = await client.listDnsRecords(String(p.subdomain));
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: JSON.stringify(result, null, 2),
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
case "lovie_create_dns_record": {
|
|
259
|
+
const result = await client.createDnsRecord({
|
|
260
|
+
subdomain: String(p.subdomain),
|
|
261
|
+
type: p.type,
|
|
262
|
+
name: String(p.name),
|
|
263
|
+
content: String(p.content),
|
|
264
|
+
ttl: p.ttl ? Number(p.ttl) : undefined,
|
|
265
|
+
priority: p.priority ? Number(p.priority) : undefined,
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: JSON.stringify(result, null, 2),
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
case "lovie_delete_dns_record": {
|
|
277
|
+
const result = await client.deleteDnsRecord(String(p.id));
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: JSON.stringify(result, null, 2),
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
case "lovie_list_presets": {
|
|
288
|
+
const result = await client.listPresets();
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: JSON.stringify(result, null, 2),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
case "lovie_apply_preset": {
|
|
299
|
+
const result = await client.applyPreset({
|
|
300
|
+
subdomain: String(p.subdomain),
|
|
301
|
+
preset: String(p.preset),
|
|
302
|
+
customTarget: p.customTarget ? String(p.customTarget) : undefined,
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
content: [
|
|
306
|
+
{
|
|
307
|
+
type: "text",
|
|
308
|
+
text: JSON.stringify(result, null, 2),
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
case "lovie_verify_doh": {
|
|
314
|
+
const result = await client.verifyDoH(String(p.domain), p.type ? String(p.type) : "A");
|
|
315
|
+
return {
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: JSON.stringify(result, null, 2),
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
case "lovie_get_profile": {
|
|
325
|
+
const result = await client.getProfile();
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: JSON.stringify(result, null, 2),
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
default:
|
|
336
|
+
return {
|
|
337
|
+
isError: true,
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: "text",
|
|
341
|
+
text: `Unknown tool: ${name}`,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
349
|
+
return {
|
|
350
|
+
isError: true,
|
|
351
|
+
content: [
|
|
352
|
+
{
|
|
353
|
+
type: "text",
|
|
354
|
+
text: `Error executing ${name}: ${msg}`,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface LovieMeProfile {
|
|
2
|
+
id: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
plan: "starter" | "pro" | "grant";
|
|
6
|
+
quota: {
|
|
7
|
+
subdomains_max: number;
|
|
8
|
+
subdomains_used: number;
|
|
9
|
+
subdomains_remaining: number;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface LovieMeSubdomain {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
fqdn: string;
|
|
16
|
+
created_at: string;
|
|
17
|
+
dns_records_count: number;
|
|
18
|
+
email_forwarder_active: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface LovieMeDnsRecord {
|
|
21
|
+
id: string;
|
|
22
|
+
subdomain_id: string;
|
|
23
|
+
type: "A" | "AAAA" | "CNAME" | "TXT" | "MX" | "CAA";
|
|
24
|
+
name: string;
|
|
25
|
+
content: string;
|
|
26
|
+
ttl: number;
|
|
27
|
+
priority?: number | null;
|
|
28
|
+
created_at: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PresetTarget {
|
|
31
|
+
name: string;
|
|
32
|
+
type: "CNAME" | "A" | "TXT";
|
|
33
|
+
content: string;
|
|
34
|
+
ttl: number;
|
|
35
|
+
priority?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface CloudPreset {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
records: PresetTarget[];
|
|
42
|
+
docs_url: string;
|
|
43
|
+
}
|
|
44
|
+
export interface DoHResolutionResult {
|
|
45
|
+
resolver: string;
|
|
46
|
+
status: "resolved" | "not_found" | "error";
|
|
47
|
+
latency_ms: number;
|
|
48
|
+
answers: Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
type: number;
|
|
51
|
+
data: string;
|
|
52
|
+
TTL: number;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "lovieme-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "lovieme-mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Official Model Context Protocol (MCP) server for LovieMe — claim free *.lovie.me subdomains, configure Anycast DNS, and wire up cloud presets directly from AI coding agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"lovieme-mcp": "bin/lovieme-mcp.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"bin",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"test": "vitest run"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"lovieme",
|
|
25
|
+
"dns",
|
|
26
|
+
"subdomain",
|
|
27
|
+
"ai-agent",
|
|
28
|
+
"cursor",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"antigravity"
|
|
31
|
+
],
|
|
32
|
+
"author": "Sagar Kewat (Sagarithm)",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
36
|
+
"zod": "^3.24.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.17.0",
|
|
40
|
+
"typescript": "^5.8.0",
|
|
41
|
+
"vitest": "^4.1.7"
|
|
42
|
+
}
|
|
43
|
+
}
|