@silentfuture/inwx-mcp 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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/index.js +177 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SilentFuture UG (haftungsbeschraenkt)
|
|
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,88 @@
|
|
|
1
|
+
# @silentfuture/inwx-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server for the [INWX](https://www.inwx.com) domain registrar.
|
|
4
|
+
It lets Claude Code, Claude Desktop, Cursor and any other MCP client manage domains through
|
|
5
|
+
INWX's official DomRobot JSON-RPC API: list and inspect domains, check availability and prices,
|
|
6
|
+
start transfers, change nameservers, manage contact handles and INWX-hosted DNS zones.
|
|
7
|
+
|
|
8
|
+
Built by [SilentFuture](https://silentfuture.de) while moving ~100 domains from legacy hosters
|
|
9
|
+
to INWX and Cloudflare DNS. Uses the official `domrobot-client` package, no scraping.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
Add to your MCP client configuration (Claude Code: `.mcp.json` in the project, or `claude mcp add`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"inwx": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "@silentfuture/inwx-mcp"],
|
|
21
|
+
"env": {
|
|
22
|
+
"INWX_USERNAME": "your-inwx-login",
|
|
23
|
+
"INWX_PASSWORD": "your-inwx-password",
|
|
24
|
+
"INWX_SHARED_SECRET": "",
|
|
25
|
+
"INWX_ENV": "live"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Variable | Meaning |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `INWX_USERNAME` | INWX login (required) |
|
|
35
|
+
| `INWX_PASSWORD` | INWX password (required) |
|
|
36
|
+
| `INWX_SHARED_SECRET` | TOTP shared secret if two-factor auth is enabled on the account (optional) |
|
|
37
|
+
| `INWX_ENV` | `live` (default) or `ote` for the INWX test system. OTE uses a separate account from [ote.inwx.com](https://ote.inwx.com) |
|
|
38
|
+
|
|
39
|
+
Start with `INWX_ENV=ote` if you want to try the write tools without spending money.
|
|
40
|
+
|
|
41
|
+
## Tools
|
|
42
|
+
|
|
43
|
+
| Tool | Kind | DomRobot method |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `account_info` | read | `account.info` |
|
|
46
|
+
| `domain_list` | read | `domain.list` (filter, paging) |
|
|
47
|
+
| `domain_info` | read | `domain.info` (contacts, nameservers, lock, renewal mode) |
|
|
48
|
+
| `domain_check` | read | `domain.check` (up to 50 domains) |
|
|
49
|
+
| `domain_price` | read | `domain.getPrices` |
|
|
50
|
+
| `domain_log` | read | `domain.log` |
|
|
51
|
+
| `domain_transfer` | write, costs money | `domain.transfer` (auth code, nameservers, contact handles) |
|
|
52
|
+
| `domain_update` | write | `domain.update` (nameservers, transfer lock, renewal mode, contacts) |
|
|
53
|
+
| `domain_renew` | write, costs money | `domain.renew` |
|
|
54
|
+
| `contact_list`, `contact_info` | read | `contact.list`, `contact.info` |
|
|
55
|
+
| `contact_create` | write | `contact.create` |
|
|
56
|
+
| `nameserver_list`, `nameserver_info` | read | `nameserver.list`, `nameserver.info` |
|
|
57
|
+
| `api_call` | expert | any DomRobot method with raw params |
|
|
58
|
+
|
|
59
|
+
Every write tool accepts `testing: true`, which makes INWX validate the request without executing it.
|
|
60
|
+
Tool descriptions state clearly when a call costs money, so an agent can ask before acting.
|
|
61
|
+
|
|
62
|
+
## Example prompts
|
|
63
|
+
|
|
64
|
+
- "List all .de domains in my INWX account with their expiry dates."
|
|
65
|
+
- "Check whether example.com and example.io are available and what they cost per year."
|
|
66
|
+
- "Transfer example.org to INWX with auth code XYZ, set the nameservers to bingo.ns.cloudflare.com and langston.ns.cloudflare.com, use contact 12345 for all roles. Dry run first."
|
|
67
|
+
- "Switch the nameservers of all my domains that still point at ns1.oldhoster.example to Cloudflare."
|
|
68
|
+
|
|
69
|
+
## Behaviour and safety
|
|
70
|
+
|
|
71
|
+
- Logs in lazily on the first call and re-authenticates once if the INWX session expired (codes 2200/2201).
|
|
72
|
+
- Response codes 1000-1999 are success; anything else is returned as an MCP tool error with INWX's code, message and reason.
|
|
73
|
+
- Credentials are read from the environment only. Nothing is written to disk.
|
|
74
|
+
- Transfers of gTLDs add one year of runtime; .de and .eu transfers keep their runtime. .de domains need an owner with a German address or a German admin contact.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
pnpm install
|
|
80
|
+
pnpm dev # runs src/index.ts via tsx
|
|
81
|
+
pnpm build # emits dist/index.js
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requirements: Node 22+, pnpm 10.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT, SilentFuture UG (haftungsbeschraenkt). Not affiliated with INWX GmbH.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// inwx-mcp - MCP server for the INWX DomRobot JSON-RPC API.
|
|
3
|
+
//
|
|
4
|
+
// Environment:
|
|
5
|
+
// INWX_USERNAME INWX login (required)
|
|
6
|
+
// INWX_PASSWORD INWX password (required)
|
|
7
|
+
// INWX_SHARED_SECRET TOTP shared secret, only if 2FA is enabled on the account
|
|
8
|
+
// INWX_ENV "live" (default) or "ote" (test system, separate account at ote.inwx.com)
|
|
9
|
+
//
|
|
10
|
+
// Every tool talks to the API of the environment above. In "live" mode, transfers,
|
|
11
|
+
// registrations and renewals cost real money. Read tools are safe; write tools are
|
|
12
|
+
// named with a verb (transfer, update, renew, create, delete) and say so in their description.
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
14
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
15
|
+
import { ApiClient, Language } from "domrobot-client";
|
|
16
|
+
import * as z from "zod/v4";
|
|
17
|
+
const ENV = (process.env.INWX_ENV ?? "live").toLowerCase();
|
|
18
|
+
const USERNAME = process.env.INWX_USERNAME ?? "";
|
|
19
|
+
const PASSWORD = process.env.INWX_PASSWORD ?? "";
|
|
20
|
+
const SHARED_SECRET = process.env.INWX_SHARED_SECRET ?? "";
|
|
21
|
+
if (!USERNAME || !PASSWORD) {
|
|
22
|
+
console.error("inwx-mcp: INWX_USERNAME and INWX_PASSWORD must be set");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const apiUrl = ENV === "ote" ? ApiClient.API_URL_OTE : ApiClient.API_URL_LIVE;
|
|
26
|
+
const client = new ApiClient(apiUrl, Language.EN, false);
|
|
27
|
+
let loggedIn = false;
|
|
28
|
+
async function login() {
|
|
29
|
+
const r = (await client.login(USERNAME, PASSWORD, SHARED_SECRET || undefined));
|
|
30
|
+
if (r.code !== 1000)
|
|
31
|
+
throw new Error(`INWX login failed: ${r.code} ${r.msg ?? ""}`);
|
|
32
|
+
loggedIn = true;
|
|
33
|
+
}
|
|
34
|
+
// Calls the API, logging in lazily and re-logging once if the session expired (code 2200/2201).
|
|
35
|
+
async function call(method, params = {}) {
|
|
36
|
+
if (!loggedIn)
|
|
37
|
+
await login();
|
|
38
|
+
let r = (await client.callApi(method, params));
|
|
39
|
+
if (r.code === 2200 || r.code === 2201) {
|
|
40
|
+
loggedIn = false;
|
|
41
|
+
await login();
|
|
42
|
+
r = (await client.callApi(method, params));
|
|
43
|
+
}
|
|
44
|
+
if (r.code < 1000 || r.code >= 2000) {
|
|
45
|
+
const detail = [r.msg, r.reasonCode, r.reason].filter(Boolean).join(" | ");
|
|
46
|
+
throw new Error(`INWX ${method} failed: ${r.code} ${detail}`);
|
|
47
|
+
}
|
|
48
|
+
return (r.resData ?? {});
|
|
49
|
+
}
|
|
50
|
+
const text = (v) => ({ content: [{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) }] });
|
|
51
|
+
const fail = (e) => ({ isError: true, content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }] });
|
|
52
|
+
const run = async (fn) => { try {
|
|
53
|
+
return text(await fn());
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
return fail(e);
|
|
57
|
+
} };
|
|
58
|
+
const nsSchema = z.array(z.string().min(3)).min(2).max(8).describe("Nameserver hostnames, at least two");
|
|
59
|
+
serveStdio(() => {
|
|
60
|
+
const server = new McpServer({ name: "inwx", version: "0.1.0" });
|
|
61
|
+
// ---------- account ----------
|
|
62
|
+
server.registerTool("account_info", {
|
|
63
|
+
description: `Account details and prepaid balance of the INWX account (${ENV}). Read-only.`,
|
|
64
|
+
inputSchema: z.object({}),
|
|
65
|
+
}, async () => run(() => call("account.info")));
|
|
66
|
+
// ---------- domains ----------
|
|
67
|
+
server.registerTool("domain_list", {
|
|
68
|
+
description: "List domains in the account with status, nameservers, renewal mode and expiry. Read-only. Filter by substring or TLD.",
|
|
69
|
+
inputSchema: z.object({
|
|
70
|
+
domain: z.string().optional().describe("Substring / wildcard filter, e.g. '*.de'"),
|
|
71
|
+
page: z.number().int().min(1).default(1),
|
|
72
|
+
pagelimit: z.number().int().min(1).max(500).default(200),
|
|
73
|
+
}),
|
|
74
|
+
}, async ({ domain, page, pagelimit }) => run(() => call("domain.list", { ...(domain ? { domain } : {}), page, pagelimit })));
|
|
75
|
+
server.registerTool("domain_info", {
|
|
76
|
+
description: "Full details of one domain in the account: status, contacts, nameservers, transfer lock, renewal mode, auth code where the registry exposes it. Read-only.",
|
|
77
|
+
inputSchema: z.object({ domain: z.string() }),
|
|
78
|
+
}, async ({ domain }) => run(() => call("domain.info", { domain, wide: 2 })));
|
|
79
|
+
server.registerTool("domain_check", {
|
|
80
|
+
description: "Availability check for one or more domains (registered elsewhere, free, or transferable). Read-only.",
|
|
81
|
+
inputSchema: z.object({ domains: z.array(z.string()).min(1).max(50) }),
|
|
82
|
+
}, async ({ domains }) => run(() => call("domain.check", { domain: domains })));
|
|
83
|
+
server.registerTool("domain_price", {
|
|
84
|
+
description: "Price list for TLDs (registration, renewal, transfer) in the account currency. Read-only.",
|
|
85
|
+
inputSchema: z.object({ tlds: z.array(z.string()).optional().describe("e.g. ['de','com'], empty = all") }),
|
|
86
|
+
}, async ({ tlds }) => run(() => call("domain.getPrices", tlds?.length ? { tld: tlds } : {})));
|
|
87
|
+
server.registerTool("domain_transfer", {
|
|
88
|
+
description: "WRITE, COSTS MONEY: start an inbound transfer of a domain to INWX. Needs the auth code from the current registrar, four contact handle ids (may be identical) and the nameservers to set. gTLDs add one year of runtime, .de/.eu keep theirs.",
|
|
89
|
+
inputSchema: z.object({
|
|
90
|
+
domain: z.string(),
|
|
91
|
+
authCode: z.string().min(1),
|
|
92
|
+
ns: nsSchema,
|
|
93
|
+
registrant: z.number().int().describe("Contact handle id for the owner"),
|
|
94
|
+
admin: z.number().int().optional().describe("Defaults to registrant"),
|
|
95
|
+
tech: z.number().int().optional().describe("Defaults to registrant"),
|
|
96
|
+
billing: z.number().int().optional().describe("Defaults to registrant"),
|
|
97
|
+
renewalMode: z.enum(["AUTORENEW", "AUTOEXPIRE", "AUTODELETE"]).default("AUTORENEW"),
|
|
98
|
+
testing: z.boolean().default(false).describe("true = validate only, do not execute"),
|
|
99
|
+
}),
|
|
100
|
+
}, async (p) => run(() => call("domain.transfer", {
|
|
101
|
+
domain: p.domain, authCode: p.authCode, ns: p.ns,
|
|
102
|
+
registrant: p.registrant, admin: p.admin ?? p.registrant, tech: p.tech ?? p.registrant, billing: p.billing ?? p.registrant,
|
|
103
|
+
renewalMode: p.renewalMode, testing: p.testing,
|
|
104
|
+
})));
|
|
105
|
+
server.registerTool("domain_update", {
|
|
106
|
+
description: "WRITE: change nameservers, transfer lock, renewal mode or contacts of a domain already at INWX. Only the given fields change.",
|
|
107
|
+
inputSchema: z.object({
|
|
108
|
+
domain: z.string(),
|
|
109
|
+
ns: nsSchema.optional(),
|
|
110
|
+
transferLock: z.boolean().optional(),
|
|
111
|
+
renewalMode: z.enum(["AUTORENEW", "AUTOEXPIRE", "AUTODELETE"]).optional(),
|
|
112
|
+
registrant: z.number().int().optional(),
|
|
113
|
+
admin: z.number().int().optional(),
|
|
114
|
+
tech: z.number().int().optional(),
|
|
115
|
+
billing: z.number().int().optional(),
|
|
116
|
+
testing: z.boolean().default(false),
|
|
117
|
+
}),
|
|
118
|
+
}, async (p) => run(() => {
|
|
119
|
+
const params = { domain: p.domain, testing: p.testing };
|
|
120
|
+
for (const k of ["ns", "renewalMode", "registrant", "admin", "tech", "billing"])
|
|
121
|
+
if (p[k] !== undefined)
|
|
122
|
+
params[k] = p[k];
|
|
123
|
+
if (p.transferLock !== undefined)
|
|
124
|
+
params.transferLock = p.transferLock ? 1 : 0;
|
|
125
|
+
return call("domain.update", params);
|
|
126
|
+
}));
|
|
127
|
+
server.registerTool("domain_renew", {
|
|
128
|
+
description: "WRITE, COSTS MONEY: renew a domain at INWX by N years.",
|
|
129
|
+
inputSchema: z.object({ domain: z.string(), period: z.string().default("1Y").describe("e.g. '1Y'"), testing: z.boolean().default(false) }),
|
|
130
|
+
}, async ({ domain, period, testing }) => run(async () => {
|
|
131
|
+
const info = await call("domain.info", { domain });
|
|
132
|
+
return call("domain.renew", { domain, period, expiration: info.exDate, testing });
|
|
133
|
+
}));
|
|
134
|
+
server.registerTool("domain_log", {
|
|
135
|
+
description: "Recent log entries for a domain (transfers, updates, registry messages). Read-only.",
|
|
136
|
+
inputSchema: z.object({ domain: z.string(), pagelimit: z.number().int().min(1).max(200).default(50) }),
|
|
137
|
+
}, async ({ domain, pagelimit }) => run(() => call("domain.log", { domain, pagelimit })));
|
|
138
|
+
// ---------- contacts ----------
|
|
139
|
+
server.registerTool("contact_list", {
|
|
140
|
+
description: "List contact handles (owner/admin/tech/billing) in the account. Read-only.",
|
|
141
|
+
inputSchema: z.object({}),
|
|
142
|
+
}, async () => run(() => call("contact.list", { pagelimit: 200 })));
|
|
143
|
+
server.registerTool("contact_info", {
|
|
144
|
+
description: "Details of one contact handle. Read-only.",
|
|
145
|
+
inputSchema: z.object({ id: z.number().int() }),
|
|
146
|
+
}, async ({ id }) => run(() => call("contact.info", { id, wide: 1 })));
|
|
147
|
+
server.registerTool("contact_create", {
|
|
148
|
+
description: "WRITE: create a contact handle (ORG or PERSON). For .de the owner needs a German address or a German admin contact.",
|
|
149
|
+
inputSchema: z.object({
|
|
150
|
+
type: z.enum(["ORG", "PERSON", "ROLE"]),
|
|
151
|
+
name: z.string(),
|
|
152
|
+
org: z.string().optional(),
|
|
153
|
+
street: z.string(),
|
|
154
|
+
city: z.string(),
|
|
155
|
+
pc: z.string().describe("Postal code"),
|
|
156
|
+
cc: z.string().length(2).describe("Country code, e.g. DE"),
|
|
157
|
+
voice: z.string().describe("+49.20212345678"),
|
|
158
|
+
email: z.string().email(),
|
|
159
|
+
testing: z.boolean().default(false),
|
|
160
|
+
}),
|
|
161
|
+
}, async (p) => run(() => call("contact.create", p)));
|
|
162
|
+
// ---------- nameserver zones (only if DNS is hosted at INWX) ----------
|
|
163
|
+
server.registerTool("nameserver_list", {
|
|
164
|
+
description: "List DNS zones hosted on INWX nameservers. Read-only. Not needed for domains whose DNS lives at Cloudflare.",
|
|
165
|
+
inputSchema: z.object({ domain: z.string().optional() }),
|
|
166
|
+
}, async ({ domain }) => run(() => call("nameserver.list", { ...(domain ? { domain } : {}), pagelimit: 200 })));
|
|
167
|
+
server.registerTool("nameserver_info", {
|
|
168
|
+
description: "Records of one INWX-hosted DNS zone. Read-only.",
|
|
169
|
+
inputSchema: z.object({ domain: z.string() }),
|
|
170
|
+
}, async ({ domain }) => run(() => call("nameserver.info", { domain })));
|
|
171
|
+
// ---------- escape hatch ----------
|
|
172
|
+
server.registerTool("api_call", {
|
|
173
|
+
description: "Expert tool: call any DomRobot method by name with raw params (see https://www.inwx.de/en/help/apidoc). WRITE methods execute for real in live mode; prefer the named tools.",
|
|
174
|
+
inputSchema: z.object({ method: z.string().regex(/^[a-z]+\.[A-Za-z]+$/), params: z.record(z.string(), z.unknown()).default({}) }),
|
|
175
|
+
}, async ({ method, params }) => run(() => call(method, params)));
|
|
176
|
+
return server;
|
|
177
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@silentfuture/inwx-mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for the INWX domain registrar: list, check, transfer and update domains, nameservers, contacts and DNS via the official DomRobot JSON-RPC API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"inwx-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "SilentFuture UG (haftungsbeschraenkt) <info@silentfuture.de> (https://silentfuture.de)",
|
|
19
|
+
"homepage": "https://github.com/SilentFuture/inwx-mcp#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/SilentFuture/inwx-mcp.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/SilentFuture/inwx-mcp/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"inwx",
|
|
31
|
+
"domrobot",
|
|
32
|
+
"domain",
|
|
33
|
+
"registrar",
|
|
34
|
+
"dns",
|
|
35
|
+
"nameserver",
|
|
36
|
+
"transfer",
|
|
37
|
+
"claude",
|
|
38
|
+
"ai-agent"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
45
|
+
"domrobot-client": "3.3.0",
|
|
46
|
+
"zod": "4.5.4"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.15.0",
|
|
50
|
+
"tsx": "^4.20.0",
|
|
51
|
+
"typescript": "^5.8.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -p tsconfig.json",
|
|
55
|
+
"dev": "tsx src/index.ts",
|
|
56
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
57
|
+
"start": "node dist/index.js"
|
|
58
|
+
}
|
|
59
|
+
}
|