@suzko/mcp-server 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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * MCP Prompts — workflow templates for common tasks.
3
+ */
4
+ import { z } from "zod";
5
+ export function registerPrompts(server) {
6
+ server.prompt("deploy-app", "Deploy an application to Suzko cloud", {
7
+ appType: z.string().optional().describe("Type of app (e.g., next.js, express, static, docker)"),
8
+ database: z.string().optional().describe("Database needed (postgres, mysql, mongodb, none)"),
9
+ }, async ({ appType, database }) => ({
10
+ messages: [{
11
+ role: "user",
12
+ content: {
13
+ type: "text",
14
+ text: `I want to deploy ${appType || "my application"} to Suzko's cloud platform.${database && database !== "none"
15
+ ? ` I need a ${database} database.`
16
+ : ""}
17
+
18
+ Please help me:
19
+ 1. Check what templates are available using list_deploy_templates
20
+ 2. Create a deploy project with the right template
21
+ 3. If I need a database, set one up and give me the connection string
22
+ 4. Help me configure environment variables
23
+ 5. Make sure everything is running
24
+
25
+ Start by listing the available templates.`,
26
+ },
27
+ }],
28
+ }));
29
+ server.prompt("find-domain", "Search and register a domain name", {
30
+ keywords: z.string().describe("Keywords for the domain (e.g., 'my startup', 'tech blog')"),
31
+ tld: z.string().optional().describe("Preferred TLD (e.g., .com, .dev, .io)"),
32
+ }, async ({ keywords, tld }) => ({
33
+ messages: [{
34
+ role: "user",
35
+ content: {
36
+ type: "text",
37
+ text: `I'm looking for a domain name. My keywords are: "${keywords}"${tld ? `. I prefer ${tld} domains.` : ""}
38
+
39
+ Please:
40
+ 1. Search for available domains using search_domains
41
+ 2. Suggest creative alternatives using suggest_domain_names
42
+ 3. Show me pricing for the best options
43
+ 4. Help me register my favorite one`,
44
+ },
45
+ }],
46
+ }));
47
+ server.prompt("troubleshoot-service", "Diagnose and fix issues with a deployed service", {
48
+ projectId: z.string().optional().describe("Deploy project ID or name"),
49
+ issue: z.string().optional().describe("Description of the problem"),
50
+ }, async ({ projectId, issue }) => ({
51
+ messages: [{
52
+ role: "user",
53
+ content: {
54
+ type: "text",
55
+ text: `I'm having issues with ${projectId ? `project ${projectId}` : "one of my deployed services"}.${issue ? ` The problem: ${issue}` : ""}
56
+
57
+ Please help me troubleshoot:
58
+ 1. List my projects to find the right one
59
+ 2. Check the project status and details
60
+ 3. Read the logs for errors
61
+ 4. Check build logs if it's a deployment issue
62
+ 5. Suggest fixes based on what you find`,
63
+ },
64
+ }],
65
+ }));
66
+ server.prompt("setup-server", "Set up a new server (BYOS - Bring Your Own Server)", {
67
+ host: z.string().describe("Server IP or hostname"),
68
+ user: z.string().optional().describe("SSH username (default: root)"),
69
+ purpose: z.string().optional().describe("What the server is for (e.g., web app, database, docker host)"),
70
+ }, async ({ host, user, purpose }) => ({
71
+ messages: [{
72
+ role: "user",
73
+ content: {
74
+ type: "text",
75
+ text: `I have a server at ${host} (user: ${user || "root"}).${purpose ? ` I want to use it for: ${purpose}` : ""}
76
+
77
+ Please help me set it up:
78
+ 1. Connect to the server using connect_server
79
+ 2. Inspect it to see what's already installed
80
+ 3. Install Docker if needed
81
+ 4. Set up the appropriate services for my use case
82
+ 5. Configure SSL if it's a web server
83
+ 6. Make sure everything is secure (firewall, etc.)`,
84
+ },
85
+ }],
86
+ }));
87
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * MCP Resources — read-only data the AI can reference.
3
+ * Uses suzko:// URI scheme.
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import type { SuzkoClient } from "../client.js";
7
+ export declare function registerResources(server: McpServer, client: SuzkoClient): void;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * MCP Resources — read-only data the AI can reference.
3
+ * Uses suzko:// URI scheme.
4
+ */
5
+ export function registerResources(server, client) {
6
+ // Resource: List of active services
7
+ server.resource("services", "suzko://services", { description: "Your active hosting services" }, async () => {
8
+ try {
9
+ const res = await client.get("/api/client/services");
10
+ return {
11
+ contents: [{
12
+ uri: "suzko://services",
13
+ mimeType: "application/json",
14
+ text: JSON.stringify(res.data || [], null, 2),
15
+ }],
16
+ };
17
+ }
18
+ catch {
19
+ return { contents: [{ uri: "suzko://services", mimeType: "text/plain", text: "Failed to load services" }] };
20
+ }
21
+ });
22
+ // Resource: Deploy projects
23
+ server.resource("deploy-projects", "suzko://deploy/projects", { description: "Your deployed projects and containers" }, async () => {
24
+ try {
25
+ const res = await client.get("/api/deploy/projects");
26
+ return {
27
+ contents: [{
28
+ uri: "suzko://deploy/projects",
29
+ mimeType: "application/json",
30
+ text: JSON.stringify(res.data || [], null, 2),
31
+ }],
32
+ };
33
+ }
34
+ catch {
35
+ return { contents: [{ uri: "suzko://deploy/projects", mimeType: "text/plain", text: "Failed to load projects" }] };
36
+ }
37
+ });
38
+ // Resource: Domains
39
+ server.resource("domains", "suzko://domains", { description: "Your registered domains" }, async () => {
40
+ try {
41
+ const res = await client.get("/api/client/domains");
42
+ return {
43
+ contents: [{
44
+ uri: "suzko://domains",
45
+ mimeType: "application/json",
46
+ text: JSON.stringify(res.data || [], null, 2),
47
+ }],
48
+ };
49
+ }
50
+ catch {
51
+ return { contents: [{ uri: "suzko://domains", mimeType: "text/plain", text: "Failed to load domains" }] };
52
+ }
53
+ });
54
+ // Resource: Invoices
55
+ server.resource("invoices", "suzko://invoices", { description: "Your billing history and outstanding invoices" }, async () => {
56
+ try {
57
+ const res = await client.get("/api/client/invoices");
58
+ return {
59
+ contents: [{
60
+ uri: "suzko://invoices",
61
+ mimeType: "application/json",
62
+ text: JSON.stringify(res.invoices || [], null, 2),
63
+ }],
64
+ };
65
+ }
66
+ catch {
67
+ return { contents: [{ uri: "suzko://invoices", mimeType: "text/plain", text: "Failed to load invoices" }] };
68
+ }
69
+ });
70
+ // Resource: Subscription perks
71
+ server.resource("perks", "suzko://perks", { description: "Your active subscription perks and features" }, async () => {
72
+ try {
73
+ const res = await client.get("/api/store/subscriptions/perks");
74
+ return {
75
+ contents: [{
76
+ uri: "suzko://perks",
77
+ mimeType: "application/json",
78
+ text: JSON.stringify(res.data || {}, null, 2),
79
+ }],
80
+ };
81
+ }
82
+ catch {
83
+ return { contents: [{ uri: "suzko://perks", mimeType: "text/plain", text: "Failed to load perks" }] };
84
+ }
85
+ });
86
+ }
@@ -0,0 +1,18 @@
1
+ /** Validate the API base URL: must be HTTPS on an allowlisted host. */
2
+ export declare function validateApiBase(input: string | undefined | null): string;
3
+ /** POSIX single-quote escape for shell command interpolation. */
4
+ export declare function shellQuote(value: string): string;
5
+ /** Validate an absolute POSIX path. Rejects traversal, NULs, newlines. */
6
+ export declare function assertSafePath(path: string, opts?: {
7
+ allowedPrefixes?: string[];
8
+ }): string;
9
+ /** Validate a fully-qualified domain name. */
10
+ export declare function assertSafeDomain(domain: string): string;
11
+ /** Validate an email address. */
12
+ export declare function assertSafeEmail(email: string): string;
13
+ /** Validate a single DNS label (1-63 chars, alnum + hyphen). */
14
+ export declare function assertSafeSubdomain(subdomain: string): string;
15
+ /** Validate a DNS record name. */
16
+ export declare function assertSafeDnsName(name: string): string;
17
+ /** Validate a DNS record value. */
18
+ export declare function assertSafeDnsContent(content: string, maxLen?: number): string;
@@ -0,0 +1,108 @@
1
+ const ALLOWED_API_HOSTS = new Set([
2
+ "www.suzko.com",
3
+ "dev.suzko.net",
4
+ ]);
5
+ /** Validate the API base URL: must be HTTPS on an allowlisted host. */
6
+ export function validateApiBase(input) {
7
+ if (!input || typeof input !== "string") {
8
+ throw new Error("Invalid SUZKO_API_BASE: empty or not a string.");
9
+ }
10
+ let url;
11
+ try {
12
+ url = new URL(input);
13
+ }
14
+ catch {
15
+ throw new Error(`Invalid SUZKO_API_BASE "${input}": not a valid URL.`);
16
+ }
17
+ if (url.protocol !== "https:") {
18
+ throw new Error(`Invalid SUZKO_API_BASE "${input}": must use https://.`);
19
+ }
20
+ if (!ALLOWED_API_HOSTS.has(url.hostname)) {
21
+ const allowed = Array.from(ALLOWED_API_HOSTS).join(", ");
22
+ throw new Error(`Invalid SUZKO_API_BASE "${input}": hostname "${url.hostname}" is not allowed. Expected one of: ${allowed}.`);
23
+ }
24
+ return url.origin;
25
+ }
26
+ /** POSIX single-quote escape for shell command interpolation. */
27
+ export function shellQuote(value) {
28
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
29
+ }
30
+ /** Validate an absolute POSIX path. Rejects traversal, NULs, newlines. */
31
+ export function assertSafePath(path, opts = {}) {
32
+ if (typeof path !== "string" || path.length === 0) {
33
+ throw new Error("Path must be a non-empty string.");
34
+ }
35
+ if (path.includes("\0")) {
36
+ throw new Error("Path contains a null byte.");
37
+ }
38
+ if (/[\r\n]/.test(path)) {
39
+ throw new Error("Path contains a newline.");
40
+ }
41
+ if (!path.startsWith("/")) {
42
+ throw new Error(`Path "${path}" must be absolute.`);
43
+ }
44
+ if (path.split("/").includes("..")) {
45
+ throw new Error(`Path "${path}" contains a traversal segment.`);
46
+ }
47
+ if (opts.allowedPrefixes && opts.allowedPrefixes.length > 0) {
48
+ const ok = opts.allowedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : p + "/"));
49
+ if (!ok) {
50
+ throw new Error(`Path "${path}" must start with one of: ${opts.allowedPrefixes.join(", ")}.`);
51
+ }
52
+ }
53
+ return path;
54
+ }
55
+ /** Validate a fully-qualified domain name. */
56
+ export function assertSafeDomain(domain) {
57
+ if (typeof domain !== "string" || domain.length === 0 || domain.length > 253) {
58
+ throw new Error("Domain must be a non-empty string under 254 chars.");
59
+ }
60
+ if (!/^(?=.{1,253}$)(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/i.test(domain)) {
61
+ throw new Error(`Invalid domain "${domain}".`);
62
+ }
63
+ return domain;
64
+ }
65
+ /** Validate an email address. */
66
+ export function assertSafeEmail(email) {
67
+ if (typeof email !== "string" || email.length === 0 || email.length > 254) {
68
+ throw new Error("Email must be a non-empty string under 255 chars.");
69
+ }
70
+ if (!/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(email)) {
71
+ throw new Error(`Invalid email "${email}".`);
72
+ }
73
+ if (/[\r\n\s]/.test(email)) {
74
+ throw new Error("Email contains whitespace.");
75
+ }
76
+ return email;
77
+ }
78
+ /** Validate a single DNS label (1-63 chars, alnum + hyphen). */
79
+ export function assertSafeSubdomain(subdomain) {
80
+ if (typeof subdomain !== "string" ||
81
+ !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(subdomain)) {
82
+ throw new Error(`Invalid subdomain "${subdomain}".`);
83
+ }
84
+ return subdomain;
85
+ }
86
+ /** Validate a DNS record name. */
87
+ export function assertSafeDnsName(name) {
88
+ if (typeof name !== "string" || name.length === 0 || name.length > 253) {
89
+ throw new Error("DNS name must be 1-253 chars.");
90
+ }
91
+ if (!/^[A-Za-z0-9._@*-]+$/.test(name)) {
92
+ throw new Error(`Invalid DNS name "${name}".`);
93
+ }
94
+ return name;
95
+ }
96
+ /** Validate a DNS record value. */
97
+ export function assertSafeDnsContent(content, maxLen = 2048) {
98
+ if (typeof content !== "string" || content.length === 0) {
99
+ throw new Error("DNS content must be a non-empty string.");
100
+ }
101
+ if (content.length > maxLen) {
102
+ throw new Error(`DNS content too long (max ${maxLen} chars).`);
103
+ }
104
+ if (/[\0\r\n]/.test(content)) {
105
+ throw new Error("DNS content contains a NUL or newline.");
106
+ }
107
+ return content;
108
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SuzkoClient } from "../client.js";
3
+ export declare function registerAccountTools(server: McpServer, client: SuzkoClient): void;
@@ -0,0 +1,254 @@
1
+ import { z } from "zod";
2
+ export function registerAccountTools(server, client) {
3
+ // 1. Get account info
4
+ server.tool("get_account_info", "Get the authenticated user's profile information including name, email, company, and currency.", {}, async () => {
5
+ const res = await client.get("/api/auth/device-session");
6
+ if (!res.success) {
7
+ return {
8
+ content: [{ type: "text", text: `Error: ${res.error || "Failed to get account info"}` }],
9
+ isError: true,
10
+ };
11
+ }
12
+ const u = res.data;
13
+ if (!u) {
14
+ return {
15
+ content: [{ type: "text", text: "No user data returned." }],
16
+ isError: true,
17
+ };
18
+ }
19
+ const lines = [
20
+ `**Account Info**`,
21
+ `- **Name:** ${u.firstName} ${u.lastName}`,
22
+ `- **Email:** ${u.email}`,
23
+ u.companyName ? `- **Company:** ${u.companyName}` : null,
24
+ `- **Client ID:** ${u.id}`,
25
+ u.currencyCode ? `- **Currency:** ${u.currencyCode}` : null,
26
+ u.credit ? `- **Credit Balance:** ${u.credit}` : null,
27
+ ].filter(Boolean);
28
+ return {
29
+ content: [{ type: "text", text: lines.join("\n") }],
30
+ };
31
+ });
32
+ // 2. List invoices
33
+ server.tool("list_invoices", "List invoices for the authenticated user. Optionally filter by status (Paid, Unpaid, Cancelled, Refunded).", {
34
+ status: z
35
+ .string()
36
+ .optional()
37
+ .describe("Filter by invoice status: Paid, Unpaid, Cancelled, Refunded"),
38
+ page: z
39
+ .number()
40
+ .optional()
41
+ .describe("Page number (each page is 25 invoices, starting from 1)"),
42
+ }, async ({ status, page }) => {
43
+ const params = {};
44
+ if (status)
45
+ params.status = status;
46
+ if (page && page > 1)
47
+ params.limitstart = String((page - 1) * 25);
48
+ const res = await client.get("/api/invoices", params);
49
+ if (res.error) {
50
+ return {
51
+ content: [{ type: "text", text: `Error: ${res.error}` }],
52
+ isError: true,
53
+ };
54
+ }
55
+ const invoices = res.invoices ?? [];
56
+ if (invoices.length === 0) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text",
61
+ text: status
62
+ ? `No invoices found with status "${status}".`
63
+ : "No invoices found.",
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ const lines = invoices.map((inv) => {
69
+ const bal = inv.balance && inv.balance !== inv.total ? ` (balance: ${inv.balance})` : "";
70
+ return `• **#${inv.invoicenum || inv.id}** — ${inv.status} | ${inv.total}${bal} | Due: ${inv.duedate}`;
71
+ });
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: `**Invoices (${invoices.length}${res.totalresults ? ` of ${res.totalresults}` : ""}):**\n\n${lines.join("\n")}`,
77
+ },
78
+ ],
79
+ };
80
+ });
81
+ // 3. Get invoice details
82
+ server.tool("get_invoice", "Get detailed information about a specific invoice including line items and payment info.", {
83
+ invoiceId: z.number().describe("The invoice ID to retrieve"),
84
+ }, async ({ invoiceId }) => {
85
+ const res = await client.get("/api/invoices", { id: String(invoiceId) });
86
+ if (res.error || !res.invoice) {
87
+ return {
88
+ content: [{ type: "text", text: `Error: ${res.error || "Invoice not found"}` }],
89
+ isError: true,
90
+ };
91
+ }
92
+ const inv = res.invoice;
93
+ const items = inv.items?.item ?? [];
94
+ const header = [
95
+ `**Invoice #${inv.invoicenum || inv.invoiceid || inv.id}**`,
96
+ `- **Status:** ${inv.status}`,
97
+ `- **Date:** ${inv.date}`,
98
+ `- **Due Date:** ${inv.duedate}`,
99
+ inv.datepaid ? `- **Date Paid:** ${inv.datepaid}` : null,
100
+ `- **Total:** ${inv.total}`,
101
+ inv.balance ? `- **Balance:** ${inv.balance}` : null,
102
+ inv.subtotal ? `- **Subtotal:** ${inv.subtotal}` : null,
103
+ inv.tax && inv.tax !== "0.00" ? `- **Tax:** ${inv.tax}` : null,
104
+ inv.credit && inv.credit !== "0.00" ? `- **Credit Applied:** ${inv.credit}` : null,
105
+ inv.paymentmethod ? `- **Payment Method:** ${inv.paymentmethod}` : null,
106
+ inv.notes ? `- **Notes:** ${inv.notes}` : null,
107
+ ].filter(Boolean);
108
+ let itemsText = "";
109
+ if (items.length > 0) {
110
+ const itemLines = items.map((it) => ` • ${it.description} — ${it.amount}`);
111
+ itemsText = `\n\n**Line Items (${items.length}):**\n${itemLines.join("\n")}`;
112
+ }
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: header.join("\n") + itemsText,
118
+ },
119
+ ],
120
+ };
121
+ });
122
+ // 4. List subscriptions
123
+ server.tool("list_subscriptions", "List the user's active local subscriptions (AI, Deploy, VoIP, Marketing, etc.).", {}, async () => {
124
+ const res = await client.get("/api/store/subscriptions");
125
+ if (!res.success) {
126
+ return {
127
+ content: [{ type: "text", text: `Error: ${res.error || "Failed to fetch subscriptions"}` }],
128
+ isError: true,
129
+ };
130
+ }
131
+ const subs = res.data ?? [];
132
+ if (subs.length === 0) {
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: "No active subscriptions. Visit https://www.suzko.com/store to browse available plans.",
138
+ },
139
+ ],
140
+ };
141
+ }
142
+ const lines = subs.map((s) => {
143
+ const price = s.amount != null
144
+ ? ` | ${s.currency || "$"}${(s.amount / 100).toFixed(2)}/${s.billingCycle || "mo"}`
145
+ : "";
146
+ const due = s.nextDueDate ? ` | Next due: ${s.nextDueDate}` : "";
147
+ return `• **${s.productName || s.productSlug}** — ${s.status}${price}${due}`;
148
+ });
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: `**Active Subscriptions (${subs.length}):**\n\n${lines.join("\n")}`,
154
+ },
155
+ ],
156
+ };
157
+ });
158
+ // 5. Check perks
159
+ server.tool("check_perks", "Check which subscription perks the user currently has (deploy_hobby, studio_pro, voip_basic, marketing_automation, etc.).", {}, async () => {
160
+ const res = await client.get("/api/store/subscriptions/perks");
161
+ if (!res.success) {
162
+ return {
163
+ content: [{ type: "text", text: `Error: ${res.error || "Failed to check perks"}` }],
164
+ isError: true,
165
+ };
166
+ }
167
+ const perks = res.data ?? {};
168
+ const activePerks = Object.entries(perks).filter(([, v]) => v);
169
+ if (activePerks.length === 0) {
170
+ return {
171
+ content: [
172
+ {
173
+ type: "text",
174
+ text: "No active subscription perks. Subscribe to a plan at https://www.suzko.com/store to unlock features.",
175
+ },
176
+ ],
177
+ };
178
+ }
179
+ const lines = activePerks.map(([key]) => `• \`${key}\``);
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: `**Active Perks (${activePerks.length}):**\n\n${lines.join("\n")}`,
185
+ },
186
+ ],
187
+ };
188
+ });
189
+ // 6. List orders (uses invoices as fallback since no dedicated orders endpoint)
190
+ server.tool("list_orders", "List recent orders. Shows invoices as order summary since orders map to invoices in the billing system.", {
191
+ page: z
192
+ .number()
193
+ .optional()
194
+ .describe("Page number (each page is 25 results, starting from 1)"),
195
+ }, async ({ page }) => {
196
+ const params = {};
197
+ if (page && page > 1)
198
+ params.limitstart = String((page - 1) * 25);
199
+ const res = await client.get("/api/invoices", params);
200
+ if (res.error) {
201
+ return {
202
+ content: [{ type: "text", text: `Error: ${res.error}` }],
203
+ isError: true,
204
+ };
205
+ }
206
+ const invoices = res.invoices ?? [];
207
+ if (invoices.length === 0) {
208
+ return {
209
+ content: [
210
+ { type: "text", text: "No orders found." },
211
+ ],
212
+ };
213
+ }
214
+ const lines = invoices.map((inv) => {
215
+ const bal = inv.balance && inv.balance !== inv.total ? ` (balance: ${inv.balance})` : "";
216
+ return `• **#${inv.invoicenum || inv.id}** — ${inv.status} | ${inv.total}${bal} | ${inv.date}`;
217
+ });
218
+ return {
219
+ content: [
220
+ {
221
+ type: "text",
222
+ text: `**Orders — Showing invoices as order summary (${invoices.length}${res.totalresults ? ` of ${res.totalresults}` : ""}):**\n\n${lines.join("\n")}`,
223
+ },
224
+ ],
225
+ };
226
+ });
227
+ // 7. Get account balance
228
+ server.tool("get_account_balance", "Get the user's account credit balance from their profile.", {}, async () => {
229
+ const res = await client.get("/api/auth/device-session");
230
+ if (!res.success) {
231
+ return {
232
+ content: [{ type: "text", text: `Error: ${res.error || "Failed to get balance"}` }],
233
+ isError: true,
234
+ };
235
+ }
236
+ const u = res.data;
237
+ if (!u) {
238
+ return {
239
+ content: [{ type: "text", text: "No user data returned." }],
240
+ isError: true,
241
+ };
242
+ }
243
+ const credit = u.credit || "0.00";
244
+ const currency = u.currencyCode || "USD";
245
+ return {
246
+ content: [
247
+ {
248
+ type: "text",
249
+ text: `**Account Balance:** ${credit} ${currency}\n\nThis is the credit balance on your account that can be applied to invoices.`,
250
+ },
251
+ ],
252
+ };
253
+ });
254
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SuzkoClient } from "../client.js";
3
+ export declare function registerDeployTools(server: McpServer, client: SuzkoClient): void;