@themkn/gandi-mcp 0.1.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/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -0
- package/dist/gandi/backup.d.ts +3 -0
- package/dist/gandi/backup.d.ts.map +1 -0
- package/dist/gandi/backup.js +38 -0
- package/dist/gandi/backup.js.map +1 -0
- package/dist/gandi/client.d.ts +15 -0
- package/dist/gandi/client.d.ts.map +1 -0
- package/dist/gandi/client.js +79 -0
- package/dist/gandi/client.js.map +1 -0
- package/dist/gandi/errors.d.ts +7 -0
- package/dist/gandi/errors.d.ts.map +1 -0
- package/dist/gandi/errors.js +31 -0
- package/dist/gandi/errors.js.map +1 -0
- package/dist/gandi/types.d.ts +23 -0
- package/dist/gandi/types.d.ts.map +1 -0
- package/dist/gandi/types.js +2 -0
- package/dist/gandi/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +52 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/domains.d.ts +3 -0
- package/dist/tools/domains.d.ts.map +1 -0
- package/dist/tools/domains.js +13 -0
- package/dist/tools/domains.js.map +1 -0
- package/dist/tools/records.d.ts +3 -0
- package/dist/tools/records.d.ts.map +1 -0
- package/dist/tools/records.js +122 -0
- package/dist/tools/records.js.map +1 -0
- package/dist/tools/snapshots.d.ts +3 -0
- package/dist/tools/snapshots.d.ts.map +1 -0
- package/dist/tools/snapshots.js +32 -0
- package/dist/tools/snapshots.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mkn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# gandi-mcp — Gandi LiveDNS MCP Server
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@themkn/gandi-mcp)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
An [MCP server](https://modelcontextprotocol.io) for [Gandi LiveDNS](https://api.gandi.net/docs/livedns/). Manage DNS records, list domains, and take zone snapshots — directly from [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or any MCP-compatible client.
|
|
7
|
+
|
|
8
|
+
## What this gives you
|
|
9
|
+
|
|
10
|
+
- **8 MCP tools:** `list_domains`, `list_records`, `get_record`, `add_record`, `update_record`, `delete_record`, `list_snapshots`, `create_snapshot`
|
|
11
|
+
- **Safety by default:** every `add`/`update`/`delete` writes a timestamped local JSON backup of the full zone **before** mutating. Configurable.
|
|
12
|
+
- **One-JSON config:** no env vars, one file with strict `0600` perms
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- **A Gandi account** with a [Personal Access Token](https://admin.gandi.net/) (*Account → Security → Personal Access Tokens*) with LiveDNS access
|
|
17
|
+
- **Node.js 24+**
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install -g @themkn/gandi-mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The binary is `gandi-mcp` regardless of the scoped package name.
|
|
26
|
+
|
|
27
|
+
## Configure
|
|
28
|
+
|
|
29
|
+
The server reads a JSON config from `~/.gandi-mcp/config.json`. Create it with `0600`:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
mkdir -p ~/.gandi-mcp
|
|
33
|
+
chmod 700 ~/.gandi-mcp
|
|
34
|
+
cat > ~/.gandi-mcp/config.json <<'EOF'
|
|
35
|
+
{
|
|
36
|
+
"apiKey": "YOUR_GANDI_PAT",
|
|
37
|
+
"defaultDomain": "example.com"
|
|
38
|
+
}
|
|
39
|
+
EOF
|
|
40
|
+
chmod 600 ~/.gandi-mcp/config.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Full config reference
|
|
44
|
+
|
|
45
|
+
| Field | Required | Default | Description |
|
|
46
|
+
|-------|----------|---------|-------------|
|
|
47
|
+
| `apiKey` | yes | — | Your Gandi Personal Access Token |
|
|
48
|
+
| `defaultDomain` | no | none | Used when a tool call omits `domain` |
|
|
49
|
+
| `autoBackup` | no | `true` | Write local zone backup before each mutation |
|
|
50
|
+
| `backupDir` | no | `~/.gandi-mcp/backups` | Where backup files land |
|
|
51
|
+
|
|
52
|
+
The server refuses to start if the config file is group- or world-readable.
|
|
53
|
+
|
|
54
|
+
## Hook into Claude Code
|
|
55
|
+
|
|
56
|
+
Add this to your `.mcp.json` (project-scoped) or `~/.config/claude/mcp.json` (global):
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"gandi": {
|
|
62
|
+
"command": "gandi-mcp"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Auto-approve tools
|
|
69
|
+
|
|
70
|
+
By default Claude Code prompts on every tool call. To allow all `gandi-mcp` tools without prompting, add to `~/.claude/settings.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"permissions": {
|
|
75
|
+
"allow": ["mcp__gandi__*"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Tool reference
|
|
81
|
+
|
|
82
|
+
| Tool | What it does |
|
|
83
|
+
|------|--------------|
|
|
84
|
+
| `list_domains` | List all domains on your Gandi account |
|
|
85
|
+
| `list_records` | List records for a domain, with optional `type` and `nameFilter` (anchored, case-insensitive glob — `*` = any, `?` = one) |
|
|
86
|
+
| `get_record` | Fetch a single record by name + type |
|
|
87
|
+
| `add_record` | Add a new record; auto-backup runs first |
|
|
88
|
+
| `update_record` | Replace values/TTL on an existing record; auto-backup runs first |
|
|
89
|
+
| `delete_record` | Delete a record; auto-backup runs first |
|
|
90
|
+
| `list_snapshots` | List Gandi server-side zone snapshots |
|
|
91
|
+
| `create_snapshot` | Create a server-side zone snapshot on Gandi |
|
|
92
|
+
|
|
93
|
+
Mutation tools return the local backup path in their response — reference it if you need to roll back manually.
|
|
94
|
+
|
|
95
|
+
## Backups
|
|
96
|
+
|
|
97
|
+
Each mutation writes `~/.gandi-mcp/backups/<domain>-<timestamp>.json`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"domain": "example.com",
|
|
102
|
+
"date": "2026-04-24T11:47:03.123Z",
|
|
103
|
+
"records": [ /* full zone */ ]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Format matches the `gandi` CLI's backup shape. Disable via `"autoBackup": false` in config.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
git clone git@github.com:themkn/gandi-mcp.git
|
|
113
|
+
cd gandi-mcp
|
|
114
|
+
pnpm install
|
|
115
|
+
pnpm test
|
|
116
|
+
pnpm build
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const DEFAULT_CONFIG_PATH: string;
|
|
3
|
+
declare const ConfigSchema: z.ZodObject<{
|
|
4
|
+
apiKey: z.ZodString;
|
|
5
|
+
defaultDomain: z.ZodOptional<z.ZodString>;
|
|
6
|
+
autoBackup: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
backupDir: z.ZodDefault<z.ZodString>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
10
|
+
export declare function loadConfig(path?: string): Config;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,mBAAmB,QAA+C,CAAC;AAEhF,QAAA,MAAM,YAAY;;;;;iBAKhB,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAElD,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAA4B,GAAG,MAAM,CA2CrE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export const DEFAULT_CONFIG_PATH = join(homedir(), ".gandi-mcp", "config.json");
|
|
6
|
+
const ConfigSchema = z.object({
|
|
7
|
+
apiKey: z.string().min(1, "apiKey must be a non-empty string"),
|
|
8
|
+
defaultDomain: z.string().min(1).optional(),
|
|
9
|
+
autoBackup: z.boolean().default(true),
|
|
10
|
+
backupDir: z.string().min(1).default(join(homedir(), ".gandi-mcp", "backups")),
|
|
11
|
+
});
|
|
12
|
+
export function loadConfig(path = DEFAULT_CONFIG_PATH) {
|
|
13
|
+
let stat;
|
|
14
|
+
try {
|
|
15
|
+
stat = statSync(path);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (err.code === "ENOENT") {
|
|
19
|
+
throw new Error(`Config not found at ${path}. Create it with mode 0600, e.g.\n` +
|
|
20
|
+
` mkdir -p ~/.gandi-mcp && chmod 700 ~/.gandi-mcp\n` +
|
|
21
|
+
` echo '{"apiKey":"...","defaultDomain":"example.com"}' > ${path}\n` +
|
|
22
|
+
` chmod 600 ${path}`);
|
|
23
|
+
}
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
27
|
+
const current = (stat.mode & 0o777).toString(8);
|
|
28
|
+
throw new Error(`Config file ${path} has insecure permission ${current}. Run: chmod 600 ${path}`);
|
|
29
|
+
}
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = readFileSync(path, "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw new Error(`Unable to read ${path}: ${err.message}`);
|
|
36
|
+
}
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new Error(`Failed to parse ${path}: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
const msg = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
47
|
+
throw new Error(`Invalid config (${path}): ${msg}`);
|
|
48
|
+
}
|
|
49
|
+
return result.data;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/gandi/backup.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAqB5C,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,SAAS,SAAS,EAAE,EAC7B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAoBjB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mkdir, writeFile, chmod } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function expandTilde(p) {
|
|
5
|
+
if (p === "~")
|
|
6
|
+
return homedir();
|
|
7
|
+
if (p.startsWith("~/"))
|
|
8
|
+
return join(homedir(), p.slice(2));
|
|
9
|
+
return p;
|
|
10
|
+
}
|
|
11
|
+
function assertSafeDomain(domain) {
|
|
12
|
+
if (domain.includes("/") ||
|
|
13
|
+
domain.includes("\\") ||
|
|
14
|
+
domain.split(/[/\\]/).includes("..") ||
|
|
15
|
+
domain.includes("\0") ||
|
|
16
|
+
domain === "." ||
|
|
17
|
+
domain === "..") {
|
|
18
|
+
throw new Error(`Refusing to write backup: invalid domain "${domain}"`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function writeLocalBackup(domain, records, dir) {
|
|
22
|
+
assertSafeDomain(domain);
|
|
23
|
+
const resolvedDir = expandTilde(dir);
|
|
24
|
+
await mkdir(resolvedDir, { recursive: true, mode: 0o700 });
|
|
25
|
+
const iso = new Date().toISOString(); // 2026-04-24T11:47:03.123Z
|
|
26
|
+
const safeIso = iso.replace(/[:.]/g, "-");
|
|
27
|
+
const filename = `${domain}-${safeIso}.json`;
|
|
28
|
+
const fullPath = join(resolvedDir, filename);
|
|
29
|
+
const payload = {
|
|
30
|
+
domain,
|
|
31
|
+
date: iso,
|
|
32
|
+
records,
|
|
33
|
+
};
|
|
34
|
+
await writeFile(fullPath, JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
35
|
+
await chmod(fullPath, 0o600);
|
|
36
|
+
return fullPath;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=backup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup.js","sourceRoot":"","sources":["../../src/gandi/backup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,SAAS,WAAW,CAAC,CAAS;IAC5B,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,OAAO,EAAE,CAAC;IAChC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAc;IACtC,IACE,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QACpB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QACrB,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QACpC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QACrB,MAAM,KAAK,GAAG;QACd,MAAM,KAAK,IAAI,EACf,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,6CAA6C,MAAM,GAAG,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,OAA6B,EAC7B,GAAW;IAEX,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAE3D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,2BAA2B;IACjE,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,OAAO,OAAO,CAAC;IAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAE7C,MAAM,OAAO,GAAG;QACd,MAAM;QACN,IAAI,EAAE,GAAG;QACT,OAAO;KACR,CAAC;IAEF,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7E,MAAM,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAE7B,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DnsRecord, Domain, RecordInput, Snapshot } from "./types.js";
|
|
2
|
+
export declare class GandiClient {
|
|
3
|
+
private readonly token;
|
|
4
|
+
constructor(token: string);
|
|
5
|
+
private request;
|
|
6
|
+
listDomains(): Promise<Domain[]>;
|
|
7
|
+
listRecords(domain: string): Promise<DnsRecord[]>;
|
|
8
|
+
getRecord(domain: string, name: string, type: string): Promise<DnsRecord>;
|
|
9
|
+
addRecord(domain: string, input: RecordInput): Promise<void>;
|
|
10
|
+
updateRecord(domain: string, input: RecordInput): Promise<void>;
|
|
11
|
+
deleteRecord(domain: string, name: string, type: string): Promise<void>;
|
|
12
|
+
listSnapshots(domain: string): Promise<Snapshot[]>;
|
|
13
|
+
createSnapshot(domain: string, name?: string): Promise<Snapshot>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/gandi/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAK3E,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,MAAM;YAE5B,OAAO;IA8Bf,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAIhC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAIjD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAMzE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5D,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAa/D,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvE,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAIlD,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;CAKvE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GandiError } from "./errors.js";
|
|
2
|
+
const BASE_URL = "https://api.gandi.net/v5/livedns";
|
|
3
|
+
export class GandiClient {
|
|
4
|
+
token;
|
|
5
|
+
constructor(token) {
|
|
6
|
+
this.token = token;
|
|
7
|
+
}
|
|
8
|
+
async request(path, options = {}) {
|
|
9
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
10
|
+
...options,
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${this.token}`,
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
...(options.headers ?? {}),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
let detail = response.statusText;
|
|
19
|
+
try {
|
|
20
|
+
const body = (await response.json());
|
|
21
|
+
if (body.message)
|
|
22
|
+
detail = body.message;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// body not JSON — keep statusText
|
|
26
|
+
}
|
|
27
|
+
throw new GandiError(response.status, detail);
|
|
28
|
+
}
|
|
29
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const text = await response.text();
|
|
33
|
+
if (!text)
|
|
34
|
+
return undefined;
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
async listDomains() {
|
|
38
|
+
return this.request("/domains");
|
|
39
|
+
}
|
|
40
|
+
async listRecords(domain) {
|
|
41
|
+
return this.request(`/domains/${encodeURIComponent(domain)}/records`);
|
|
42
|
+
}
|
|
43
|
+
async getRecord(domain, name, type) {
|
|
44
|
+
return this.request(`/domains/${encodeURIComponent(domain)}/records/${encodeURIComponent(name)}/${encodeURIComponent(type)}`);
|
|
45
|
+
}
|
|
46
|
+
async addRecord(domain, input) {
|
|
47
|
+
await this.request(`/domains/${encodeURIComponent(domain)}/records`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
rrset_name: input.name,
|
|
51
|
+
rrset_type: input.type,
|
|
52
|
+
rrset_values: input.values,
|
|
53
|
+
rrset_ttl: input.ttl ?? 300,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async updateRecord(domain, input) {
|
|
58
|
+
await this.request(`/domains/${encodeURIComponent(domain)}/records/${encodeURIComponent(input.name)}/${encodeURIComponent(input.type)}`, {
|
|
59
|
+
method: "PUT",
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
rrset_values: input.values,
|
|
62
|
+
rrset_ttl: input.ttl ?? 300,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async deleteRecord(domain, name, type) {
|
|
67
|
+
await this.request(`/domains/${encodeURIComponent(domain)}/records/${encodeURIComponent(name)}/${encodeURIComponent(type)}`, { method: "DELETE" });
|
|
68
|
+
}
|
|
69
|
+
async listSnapshots(domain) {
|
|
70
|
+
return this.request(`/domains/${encodeURIComponent(domain)}/snapshots`);
|
|
71
|
+
}
|
|
72
|
+
async createSnapshot(domain, name) {
|
|
73
|
+
const init = { method: "POST" };
|
|
74
|
+
if (name !== undefined)
|
|
75
|
+
init.body = JSON.stringify({ name });
|
|
76
|
+
return this.request(`/domains/${encodeURIComponent(domain)}/snapshots`, init);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/gandi/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/gandi/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,UAAW,SAAQ,KAAK;aAEjB,MAAM,EAAE,MAAM;aACd,MAAM,EAAE,MAAM;gBADd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM;IAOhC,aAAa,IAAI,MAAM;CAmBxB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class GandiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
detail;
|
|
4
|
+
constructor(status, detail) {
|
|
5
|
+
super(`Gandi API error ${status}: ${detail}`);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.detail = detail;
|
|
8
|
+
this.name = "GandiError";
|
|
9
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
10
|
+
}
|
|
11
|
+
toUserMessage() {
|
|
12
|
+
switch (this.status) {
|
|
13
|
+
case 401:
|
|
14
|
+
return "Gandi rejected the API key. Regenerate at admin.gandi.net and update ~/.gandi-mcp/config.json.";
|
|
15
|
+
case 403:
|
|
16
|
+
return "Your Gandi PAT lacks permission for this operation.";
|
|
17
|
+
case 404:
|
|
18
|
+
return `Not found: ${this.detail} — check domain/name/type are correct.`;
|
|
19
|
+
case 409:
|
|
20
|
+
return this.detail;
|
|
21
|
+
case 429:
|
|
22
|
+
return "Rate limited by Gandi. Retry in a moment.";
|
|
23
|
+
default:
|
|
24
|
+
if (this.status >= 500 && this.status < 600) {
|
|
25
|
+
return `Gandi server error (${this.status}). Try again.`;
|
|
26
|
+
}
|
|
27
|
+
return this.detail;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/gandi/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,UAAW,SAAQ,KAAK;IAEjB;IACA;IAFlB,YACkB,MAAc,EACd,MAAc;QAE9B,KAAK,CAAC,mBAAmB,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC;QAH9B,WAAM,GAAN,MAAM,CAAQ;QACd,WAAM,GAAN,MAAM,CAAQ;QAG9B,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;IAED,aAAa;QACX,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,KAAK,GAAG;gBACN,OAAO,gGAAgG,CAAC;YAC1G,KAAK,GAAG;gBACN,OAAO,qDAAqD,CAAC;YAC/D,KAAK,GAAG;gBACN,OAAO,cAAc,IAAI,CAAC,MAAM,wCAAwC,CAAC;YAC3E,KAAK,GAAG;gBACN,OAAO,IAAI,CAAC,MAAM,CAAC;YACrB,KAAK,GAAG;gBACN,OAAO,2CAA2C,CAAC;YACrD;gBACE,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;oBAC5C,OAAO,uBAAuB,IAAI,CAAC,MAAM,eAAe,CAAC;gBAC3D,CAAC;gBACD,OAAO,IAAI,CAAC,MAAM,CAAC;QACvB,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface DnsRecord {
|
|
2
|
+
readonly rrset_name: string;
|
|
3
|
+
readonly rrset_type: string;
|
|
4
|
+
readonly rrset_values: readonly string[];
|
|
5
|
+
readonly rrset_ttl: number;
|
|
6
|
+
readonly rrset_href?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Domain {
|
|
9
|
+
readonly fqdn: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Snapshot {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly name?: string;
|
|
14
|
+
readonly created_at: string;
|
|
15
|
+
readonly automatic?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface RecordInput {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly type: string;
|
|
20
|
+
readonly values: readonly string[];
|
|
21
|
+
readonly ttl?: number;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/gandi/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/gandi/types.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { GandiClient } from "./gandi/client.js";
|
|
4
|
+
import { runServer } from "./server.js";
|
|
5
|
+
import { domainTools } from "./tools/domains.js";
|
|
6
|
+
import { recordTools } from "./tools/records.js";
|
|
7
|
+
import { snapshotTools } from "./tools/snapshots.js";
|
|
8
|
+
async function main() {
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
const client = new GandiClient(config.apiKey);
|
|
11
|
+
const tools = [
|
|
12
|
+
...domainTools,
|
|
13
|
+
...recordTools,
|
|
14
|
+
...snapshotTools,
|
|
15
|
+
];
|
|
16
|
+
await runServer({ config, client, tools });
|
|
17
|
+
}
|
|
18
|
+
main().catch((err) => {
|
|
19
|
+
process.stderr.write(`gandi-mcp: ${err.message}\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAE9C,MAAM,KAAK,GAA8B;QACvC,GAAG,WAAW;QACd,GAAG,WAAW;QACd,GAAG,aAAa;KACjB,CAAC;IAEF,MAAM,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAU,EAAE,EAAE;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;IACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { type ZodTypeAny } from "zod";
|
|
3
|
+
import { GandiClient } from "./gandi/client.js";
|
|
4
|
+
import type { Config } from "./config.js";
|
|
5
|
+
export interface ToolDefinition<Input> {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
schema: ZodTypeAny;
|
|
9
|
+
handler: (input: Input, ctx: ToolContext) => Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolContext {
|
|
12
|
+
client: GandiClient;
|
|
13
|
+
config: Config;
|
|
14
|
+
}
|
|
15
|
+
export interface ServerBootstrap {
|
|
16
|
+
config: Config;
|
|
17
|
+
client: GandiClient;
|
|
18
|
+
tools: ToolDefinition<unknown>[];
|
|
19
|
+
}
|
|
20
|
+
export declare function buildServer(bootstrap: ServerBootstrap): Server;
|
|
21
|
+
export declare function runServer(bootstrap: ServerBootstrap): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAOnE,OAAO,EAAK,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,cAAc,CAAC,KAAK;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;CAClC;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,eAAe,GAAG,MAAM,CA4C9D;AAED,wBAAsB,SAAS,CAAC,SAAS,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzE"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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 { z } from "zod";
|
|
5
|
+
import { GandiError } from "./gandi/errors.js";
|
|
6
|
+
export function buildServer(bootstrap) {
|
|
7
|
+
const { config, client, tools } = bootstrap;
|
|
8
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
9
|
+
const ctx = { client, config };
|
|
10
|
+
const mcp = new Server({ name: "gandi-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
11
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
12
|
+
tools: tools.map((t) => ({
|
|
13
|
+
name: t.name,
|
|
14
|
+
description: t.description,
|
|
15
|
+
inputSchema: toInputSchema(t.schema),
|
|
16
|
+
})),
|
|
17
|
+
}));
|
|
18
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
19
|
+
const tool = byName.get(req.params.name);
|
|
20
|
+
if (!tool) {
|
|
21
|
+
return {
|
|
22
|
+
isError: true,
|
|
23
|
+
content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const parsed = tool.schema.safeParse(req.params.arguments ?? {});
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
29
|
+
return { isError: true, content: [{ type: "text", text: `Invalid input: ${msg}` }] };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const result = await tool.handler(parsed.data, ctx);
|
|
33
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const text = err instanceof GandiError
|
|
37
|
+
? err.toUserMessage()
|
|
38
|
+
: `Unexpected error: ${err.message}`;
|
|
39
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return mcp;
|
|
43
|
+
}
|
|
44
|
+
export async function runServer(bootstrap) {
|
|
45
|
+
const server = buildServer(bootstrap);
|
|
46
|
+
const transport = new StdioServerTransport();
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
}
|
|
49
|
+
function toInputSchema(schema) {
|
|
50
|
+
return z.toJSONSchema(schema, { target: "draft-7" });
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GAEvB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAqB/C,MAAM,UAAU,WAAW,CAAC,SAA0B;IACpD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,GAAG,GAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAE5C,MAAM,GAAG,GAAG,IAAI,MAAM,CACpB,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,EACvC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,GAAG,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QACzD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC7B,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;SACrC,CAAC,CAAC;KACJ,CAAC,CAAC,CAAC;IAEJ,GAAG,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACzD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;aACtE,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QACjE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3F,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;QACvF,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACpD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,GACR,GAAG,YAAY,UAAU;gBACvB,CAAC,CAAC,GAAG,CAAC,aAAa,EAAE;gBACrB,CAAC,CAAC,qBAAsB,GAAa,CAAC,OAAO,EAAE,CAAC;YACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,SAA0B;IACxD,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,aAAa,CAAC,MAAkB;IACvC,OAAO,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAwB,CAAC;AAC9E,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/tools/domains.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAInD,eAAO,MAAM,WAAW,EAAE,cAAc,CAAC,OAAO,CAAC,EAShD,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const ListDomainsInput = z.object({}).strict();
|
|
3
|
+
export const domainTools = [
|
|
4
|
+
{
|
|
5
|
+
name: "list_domains",
|
|
6
|
+
description: "List all domains on this Gandi account.",
|
|
7
|
+
schema: ListDomainsInput,
|
|
8
|
+
handler: async (_input, ctx) => {
|
|
9
|
+
return ctx.client.listDomains();
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
//# sourceMappingURL=domains.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domains.js","sourceRoot":"","sources":["../../src/tools/domains.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;AAE/C,MAAM,CAAC,MAAM,WAAW,GAA8B;IACpD;QACE,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,yCAAyC;QACtD,MAAM,EAAE,gBAAgB;QACxB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE;YAC7B,OAAO,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAClC,CAAC;KACF;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../src/tools/records.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,cAAc,CAAC;AAmDhE,eAAO,MAAM,WAAW,EAAE,cAAc,CAAC,OAAO,CAAC,EAkFhD,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { writeLocalBackup } from "../gandi/backup.js";
|
|
3
|
+
const DomainRef = z.string().min(1).optional();
|
|
4
|
+
const NameStr = z.string().min(1);
|
|
5
|
+
const TypeStr = z.string().min(1);
|
|
6
|
+
const ValuesArr = z.array(z.string().min(1)).min(1);
|
|
7
|
+
const TtlNum = z.number().int().positive().optional();
|
|
8
|
+
const TtlMutation = z.number().int().positive().default(300);
|
|
9
|
+
const ListInput = z
|
|
10
|
+
.object({
|
|
11
|
+
domain: DomainRef,
|
|
12
|
+
type: z.string().min(1).optional(),
|
|
13
|
+
nameFilter: z.string().min(1).optional(),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
const GetInput = z.object({ domain: DomainRef, name: NameStr, type: TypeStr }).strict();
|
|
17
|
+
const AddInput = z
|
|
18
|
+
.object({ domain: DomainRef, name: NameStr, type: TypeStr, values: ValuesArr, ttl: TtlMutation })
|
|
19
|
+
.strict();
|
|
20
|
+
const UpdateInput = z
|
|
21
|
+
.object({ domain: DomainRef, name: NameStr, type: TypeStr, values: ValuesArr, ttl: TtlMutation })
|
|
22
|
+
.strict();
|
|
23
|
+
const DeleteInput = z.object({ domain: DomainRef, name: NameStr, type: TypeStr }).strict();
|
|
24
|
+
function resolveDomain(input, ctx) {
|
|
25
|
+
const d = input.domain ?? ctx.config.defaultDomain;
|
|
26
|
+
if (!d) {
|
|
27
|
+
throw new Error("No domain provided and no `defaultDomain` in config. Pass `domain` explicitly or set one in ~/.gandi-mcp/config.json.");
|
|
28
|
+
}
|
|
29
|
+
return d;
|
|
30
|
+
}
|
|
31
|
+
function globToRegex(pattern) {
|
|
32
|
+
const escaped = pattern
|
|
33
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
34
|
+
.replace(/\*/g, ".*")
|
|
35
|
+
.replace(/\?/g, ".");
|
|
36
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
37
|
+
}
|
|
38
|
+
async function backupIfEnabled(ctx, domain) {
|
|
39
|
+
if (!ctx.config.autoBackup)
|
|
40
|
+
return null;
|
|
41
|
+
const records = await ctx.client.listRecords(domain);
|
|
42
|
+
return writeLocalBackup(domain, records, ctx.config.backupDir);
|
|
43
|
+
}
|
|
44
|
+
export const recordTools = [
|
|
45
|
+
{
|
|
46
|
+
name: "list_records",
|
|
47
|
+
description: "List DNS records for a domain. Optionally filter by `type` (A, CNAME, MX, TXT, ...) and `nameFilter` (anchored, case-insensitive glob: `*` = any chars, `?` = one char).",
|
|
48
|
+
schema: ListInput,
|
|
49
|
+
handler: async (input, ctx) => {
|
|
50
|
+
const i = input;
|
|
51
|
+
const domain = resolveDomain(i, ctx);
|
|
52
|
+
let records = await ctx.client.listRecords(domain);
|
|
53
|
+
if (i.type) {
|
|
54
|
+
const t = i.type.toUpperCase();
|
|
55
|
+
records = records.filter((r) => r.rrset_type.toUpperCase() === t);
|
|
56
|
+
}
|
|
57
|
+
if (i.nameFilter) {
|
|
58
|
+
const re = globToRegex(i.nameFilter);
|
|
59
|
+
records = records.filter((r) => re.test(r.rrset_name));
|
|
60
|
+
}
|
|
61
|
+
return records;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "get_record",
|
|
66
|
+
description: "Fetch a single DNS record by name and type. Throws if no record with that name and type exists — use `list_records` first if you're unsure what exists.",
|
|
67
|
+
schema: GetInput,
|
|
68
|
+
handler: async (input, ctx) => {
|
|
69
|
+
const i = input;
|
|
70
|
+
return ctx.client.getRecord(resolveDomain(i, ctx), i.name, i.type);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "add_record",
|
|
75
|
+
description: "Add a new DNS record. A local JSON backup of the full zone is written first (configurable). Verifies by re-fetching the record.",
|
|
76
|
+
schema: AddInput,
|
|
77
|
+
handler: async (input, ctx) => {
|
|
78
|
+
const i = AddInput.parse(input);
|
|
79
|
+
const domain = resolveDomain(i, ctx);
|
|
80
|
+
const backup = await backupIfEnabled(ctx, domain);
|
|
81
|
+
await ctx.client.addRecord(domain, {
|
|
82
|
+
name: i.name,
|
|
83
|
+
type: i.type,
|
|
84
|
+
values: i.values,
|
|
85
|
+
ttl: i.ttl,
|
|
86
|
+
});
|
|
87
|
+
const record = await ctx.client.getRecord(domain, i.name, i.type);
|
|
88
|
+
return { backup, record };
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "update_record",
|
|
93
|
+
description: "Replace values and/or TTL for an existing record. A local JSON backup of the full zone is written first (configurable). Verifies by re-fetching.",
|
|
94
|
+
schema: UpdateInput,
|
|
95
|
+
handler: async (input, ctx) => {
|
|
96
|
+
const i = UpdateInput.parse(input);
|
|
97
|
+
const domain = resolveDomain(i, ctx);
|
|
98
|
+
const backup = await backupIfEnabled(ctx, domain);
|
|
99
|
+
await ctx.client.updateRecord(domain, {
|
|
100
|
+
name: i.name,
|
|
101
|
+
type: i.type,
|
|
102
|
+
values: i.values,
|
|
103
|
+
ttl: i.ttl,
|
|
104
|
+
});
|
|
105
|
+
const record = await ctx.client.getRecord(domain, i.name, i.type);
|
|
106
|
+
return { backup, record };
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "delete_record",
|
|
111
|
+
description: "Delete a DNS record. Irreversible. A local JSON backup of the full zone is written first (configurable).",
|
|
112
|
+
schema: DeleteInput,
|
|
113
|
+
handler: async (input, ctx) => {
|
|
114
|
+
const i = input;
|
|
115
|
+
const domain = resolveDomain(i, ctx);
|
|
116
|
+
const backup = await backupIfEnabled(ctx, domain);
|
|
117
|
+
await ctx.client.deleteRecord(domain, i.name, i.type);
|
|
118
|
+
return { backup, deleted: { name: i.name, type: i.type } };
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.js","sourceRoot":"","sources":["../../src/tools/records.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../../src/tools/snapshots.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,cAAc,CAAC;AAiBhE,eAAO,MAAM,aAAa,EAAE,cAAc,CAAC,OAAO,CAAC,EAoBlD,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const DomainRef = z.string().min(1).optional();
|
|
3
|
+
const ListInput = z.object({ domain: DomainRef }).strict();
|
|
4
|
+
const CreateInput = z.object({ domain: DomainRef, name: z.string().min(1).optional() }).strict();
|
|
5
|
+
function resolveDomain(input, ctx) {
|
|
6
|
+
const d = input.domain ?? ctx.config.defaultDomain;
|
|
7
|
+
if (!d) {
|
|
8
|
+
throw new Error("No domain provided and no `defaultDomain` in config. Pass `domain` explicitly or set one in ~/.gandi-mcp/config.json.");
|
|
9
|
+
}
|
|
10
|
+
return d;
|
|
11
|
+
}
|
|
12
|
+
export const snapshotTools = [
|
|
13
|
+
{
|
|
14
|
+
name: "list_snapshots",
|
|
15
|
+
description: "List server-side zone snapshots Gandi has stored for a domain.",
|
|
16
|
+
schema: ListInput,
|
|
17
|
+
handler: async (input, ctx) => {
|
|
18
|
+
const i = ListInput.parse(input);
|
|
19
|
+
return ctx.client.listSnapshots(resolveDomain(i, ctx));
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "create_snapshot",
|
|
24
|
+
description: "Create a server-side zone snapshot on Gandi. Snapshots persist on Gandi's servers and can be reviewed via `list_snapshots`. Use this before risky changes you might want to roll back.",
|
|
25
|
+
schema: CreateInput,
|
|
26
|
+
handler: async (input, ctx) => {
|
|
27
|
+
const i = CreateInput.parse(input);
|
|
28
|
+
return ctx.client.createSnapshot(resolveDomain(i, ctx), i.name);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
//# sourceMappingURL=snapshots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshots.js","sourceRoot":"","sources":["../../src/tools/snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;AAE/C,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;AAC3D,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;AAEjG,SAAS,aAAa,CAAC,KAA0B,EAAE,GAAgB;IACjE,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;IACnD,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,KAAK,CACb,uHAAuH,CACxH,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAA8B;IACtD;QACE,IAAI,EAAE,gBAAgB;QACtB,WAAW,EAAE,gEAAgE;QAC7E,MAAM,EAAE,SAAS;QACjB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC5B,MAAM,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACjC,OAAO,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QACzD,CAAC;KACF;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,WAAW,EACT,wLAAwL;QAC1L,MAAM,EAAE,WAAW;QACnB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC5B,MAAM,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACnC,OAAO,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAClE,CAAC;KACF;CACF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@themkn/gandi-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Gandi LiveDNS — manage DNS records, domains, and zone snapshots from Claude Code or any MCP client.",
|
|
5
|
+
"keywords": ["mcp", "mcp-server", "model-context-protocol", "gandi", "dns", "livedns", "claude", "claude-code"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "mkn <khoa@khoa.ch>",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"bin": {
|
|
12
|
+
"gandi-mcp": "dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24"
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "tsc --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json",
|
|
25
|
+
"audit": "npm audit --audit-level=high",
|
|
26
|
+
"prepublishOnly": "pnpm build && pnpm test"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
+
"zod": "^4.3.6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.6.0",
|
|
34
|
+
"typescript": "^6.0.2",
|
|
35
|
+
"vitest": "^4.1.4"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/themkn/gandi-mcp.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": "https://github.com/themkn/gandi-mcp/issues",
|
|
42
|
+
"homepage": "https://github.com/themkn/gandi-mcp#readme",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|