ultipa-mcp 1.0.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.
@@ -0,0 +1,197 @@
1
+ import { z } from "zod";
2
+ // Topic slugs are full repo paths without the `.md` extension.
3
+ // What's in this array is what's in the ultipa-docs repo, verbatim — no hidden
4
+ // transformations. To add a new topic, copy the path from the repo and drop `.md`.
5
+ const TOPICS = [
6
+ // gql / graph pattern matching
7
+ "gql/graph-pattern-matching/graph-pattern-matching",
8
+ "gql/graph-pattern-matching/node-and-edge-patterns",
9
+ "gql/graph-pattern-matching/graph-patterns",
10
+ "gql/graph-pattern-matching/path-patterns",
11
+ "gql/graph-pattern-matching/quantified-paths",
12
+ "gql/graph-pattern-matching/questioned-paths",
13
+ "gql/graph-pattern-matching/shortest-paths",
14
+ "gql/graph-pattern-matching/cheapest-paths",
15
+ "gql/graph-pattern-matching/khop-traversal",
16
+ // gql / graph management
17
+ "gql/graph-management/graphs",
18
+ "gql/graph-management/closed-graphs",
19
+ // gql / data manipulation
20
+ "gql/data-manipulation/node-and-edge-ids",
21
+ "gql/data-manipulation/insert",
22
+ "gql/data-manipulation/insert-overwrite",
23
+ "gql/data-manipulation/upsert",
24
+ "gql/data-manipulation/merge",
25
+ "gql/data-manipulation/foreach",
26
+ // gql / querying
27
+ "gql/querying/query-composition",
28
+ // gql / functions, operators, predicates, expressions
29
+ "gql/functions/all-functions",
30
+ "gql/operators",
31
+ "gql/predicates",
32
+ "gql/expressions",
33
+ // query perfermance
34
+ "gql/query-acceleration/index",
35
+ "gql/query-acceleration/fulltext-index",
36
+ "computing-engine/introduction",
37
+ // ontology (RDF / OWL semantics for ontology-mode graphs)
38
+ "ontology/introduction",
39
+ "ontology/class-definitions",
40
+ "ontology/object-properties",
41
+ "ontology/data-properties",
42
+ // graph-algorithms
43
+ "graph-algorithms/introduction",
44
+ "graph-algorithms/running-algorithms",
45
+ // stored-produceres
46
+ "stored-procedures/quick-start",
47
+ "stored-procedures/procedure-management",
48
+ "stored-procedures/calling-procedures",
49
+ "stored-procedures/procedure-body/procedure-body-language",
50
+ ];
51
+ const REPO_BASE = "https://raw.githubusercontent.com/ultipa/ultipa-docs/main";
52
+ const TREE_API_URL = "https://api.github.com/repos/ultipa/ultipa-docs/git/trees/main?recursive=1";
53
+ // Reserved `topic` value that triggers a live fetch of the full doc page index
54
+ // (from GitHub's tree API) instead of fetching a single markdown page.
55
+ const INDEX_SLUG = "?";
56
+ // Cache the index for the process lifetime — the tree rarely changes within a
57
+ // session and unauth GitHub API has a 60 req/hour limit. On failure, clear so
58
+ // the next call retries instead of returning a poisoned rejected promise.
59
+ let cachedIndex = null;
60
+ async function fetchRepoIndex() {
61
+ if (cachedIndex)
62
+ return cachedIndex;
63
+ const promise = (async () => {
64
+ const res = await fetch(TREE_API_URL, {
65
+ headers: { Accept: "application/vnd.github+json" },
66
+ });
67
+ if (!res.ok) {
68
+ throw new Error(`GitHub tree API returned ${res.status} ${res.statusText}`);
69
+ }
70
+ const json = (await res.json());
71
+ return (json.tree ?? [])
72
+ .filter((n) => n.type === "blob" &&
73
+ typeof n.path === "string" &&
74
+ n.path.endsWith(".md") &&
75
+ n.path.includes("/"))
76
+ .map((n) => n.path.replace(/\.md$/, ""));
77
+ })();
78
+ cachedIndex = promise;
79
+ promise.catch(() => {
80
+ cachedIndex = null;
81
+ });
82
+ return promise;
83
+ }
84
+ // Convert a topic slug to its raw GitHub markdown URL — just append `.md`.
85
+ function topicToFetchUrl(topic) {
86
+ return `${REPO_BASE}/${topic}.md`;
87
+ }
88
+ // Convert a topic slug to its rendered docs URL (for human fallback links).
89
+ // `gql/graph-management/closed-graphs` → `https://www.ultipa.com/docs/gql/closed-graphs`
90
+ // (ultipa.com flattens intermediate segments; only first segment + final page slug matter.)
91
+ function topicToBrowseUrl(topic) {
92
+ const parts = topic.split("/");
93
+ const section = parts[0];
94
+ const page = parts[parts.length - 1];
95
+ return `https://www.ultipa.com/docs/${section}/${page}`;
96
+ }
97
+ function catalogResponse(prefix) {
98
+ return {
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text: `${prefix}\n\n${TOPICS.map((t) => `- ${t}`).join("\n")}`,
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ // `lookup_docs` modes — single tool, three discovery layers, pay-as-you-go:
108
+ //
109
+ // Call Behavior
110
+ // ----------------------------------- ----------------------------------------------------------
111
+ // lookup_docs() Returns the curated TOPICS cheat-sheet. No network.
112
+ // lookup_docs({ topic: "?" }) Hits GitHub tree API, returns all .md paths in the
113
+ // repo (root README excluded). Cached for process
114
+ // lifetime; on failure the cache clears so the next
115
+ // call retries.
116
+ // lookup_docs({ topic: "some/path" }) Fetches that page's raw markdown from the repo.
117
+ // lookup_docs({ topic: "wrong/path" }) 404 from GitHub → handler returns error JSON with
118
+ // fetchedUrl, fallbackUrl, and curatedEntryPoints so the
119
+ // agent can self-correct in one round trip.
120
+ //
121
+ // No allowlist gate: any path is fetchable, validation is delegated to GitHub.
122
+ export function registerDocsTools(server) {
123
+ server.tool("lookup_docs", `Look up an Ultipa documentation page by topic slug. Fetches the page's markdown content live from the public ultipa-docs repo on GitHub. Use this when you need authoritative reference on Ultipa-specific syntax, schema rules, functions, or features the model may not know fully from training data. Topic slugs are repo paths under https://github.com/ultipa/ultipa-docs without the \`.md\` extension; ANY valid path is fetchable, the list below is just curated entry points. Call WITHOUT a topic to see the cheat-sheet, or with \`topic: "?"\` to fetch the full live index of every doc page. Curated entry points: ${TOPICS.join(", ")}.`, {
124
+ topic: z
125
+ .string()
126
+ .optional()
127
+ .describe(`Any repo path under ultipa-docs without \`.md\` (e.g. \`${TOPICS[0]}\`). Pass \`?\` to fetch the full live index of every doc page from the repo. Omit to see the curated cheat-sheet of common entry points.`),
128
+ }, async ({ topic }) => {
129
+ if (!topic) {
130
+ return catalogResponse('No topic provided. Curated entry points below (you can also pass any other valid repo path, or `topic: "?"` to fetch the full live index from the repo):');
131
+ }
132
+ if (topic === INDEX_SLUG) {
133
+ try {
134
+ const all = await fetchRepoIndex();
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: `Full index of ${all.length} doc pages in ultipa-docs (pass any of these as \`topic\` to fetch its markdown):\n\n${all.map((p) => `- ${p}`).join("\n")}`,
140
+ },
141
+ ],
142
+ };
143
+ }
144
+ catch (e) {
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: JSON.stringify({
150
+ error: "Failed to fetch repo index from GitHub. Fall back to the curated entry points below or pass a guessed slug directly.",
151
+ detail: e?.message ?? String(e),
152
+ curatedEntryPoints: TOPICS,
153
+ }, null, 2),
154
+ },
155
+ ],
156
+ };
157
+ }
158
+ }
159
+ const fetchedUrl = topicToFetchUrl(topic);
160
+ const fallbackUrl = topicToBrowseUrl(topic);
161
+ try {
162
+ const res = await fetch(fetchedUrl);
163
+ if (!res.ok) {
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify({
169
+ error: `Failed to fetch '${topic}' (${res.status} ${res.statusText}). Slug is likely wrong. **Next step**: call \`lookup_docs({ topic: "?" })\` to get the full live index of every doc page in the repo, locate the actual path for what you wanted, then re-call \`lookup_docs\` with the correct slug. Do NOT guess another slug blindly — the index is the authoritative list. As a fallback, the curated entry points below may also be close to what you need.`,
170
+ fetchedUrl,
171
+ fallbackUrl,
172
+ nextStep: 'Call lookup_docs({ topic: "?" }) to fetch the full index.',
173
+ curatedEntryPoints: TOPICS,
174
+ }, null, 2),
175
+ },
176
+ ],
177
+ };
178
+ }
179
+ return { content: [{ type: "text", text: await res.text() }] };
180
+ }
181
+ catch (e) {
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: JSON.stringify({
187
+ error: `Network error fetching '${topic}'.`,
188
+ detail: e?.message ?? String(e),
189
+ fetchedUrl,
190
+ fallbackUrl,
191
+ }, null, 2),
192
+ },
193
+ ],
194
+ };
195
+ }
196
+ });
197
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerFirewallTools(server: McpServer): void;
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import { api, json } from "../helpers/api.js";
3
+ export function registerFirewallTools(server) {
4
+ server.tool("get_my_ip", "Return the public IP of the machine running this MCP server (as seen by Ultipa Cloud). Useful before `add_firewall_rule` — pass `${ip}/32` as the CIDR to allow just this machine. Note: with stdio transport (the current setup), the MCP server runs on the user's machine, so this is effectively the user's outbound IP. A future hosted MCP would return the hosting server's IP instead.", {}, async () => json(await api("/v1/instances/my-ip")));
5
+ server.tool("list_firewall_rules", "List the IP-allowlist (firewall) rules for an instance. Only applies to instances where `firewallSupported` is true (EC2-backed); free-trial / docker-host instances don't have firewalls.", { id: z.string().describe("The instance ID") }, async ({ id }) => json(await api(`/v1/instances/${id}/firewall-rules`)));
6
+ server.tool("add_firewall_rule", "Add a CIDR to the instance's IP allowlist. Use `0.0.0.0/0` to allow all (NOT recommended for production). Synchronous.", {
7
+ id: z.string().describe("The instance ID"),
8
+ cidr: z
9
+ .string()
10
+ .describe("CIDR block to allow, e.g. '203.0.113.42/32' for a single IP or '10.0.0.0/8' for a range"),
11
+ description: z
12
+ .string()
13
+ .optional()
14
+ .describe("Optional human-readable note for this rule"),
15
+ }, async ({ id, cidr, description }) => {
16
+ const body = { cidr };
17
+ if (description !== undefined)
18
+ body.description = description;
19
+ return json(await api(`/v1/instances/${id}/firewall-rules`, {
20
+ method: "POST",
21
+ body,
22
+ }));
23
+ });
24
+ server.tool("remove_firewall_rule", "Remove a firewall rule by its CIDR. The CIDR must match an existing rule exactly. Synchronous.", {
25
+ id: z.string().describe("The instance ID"),
26
+ cidr: z
27
+ .string()
28
+ .describe("CIDR of the rule to remove (must match an existing rule)"),
29
+ }, async ({ id, cidr }) => {
30
+ await api(`/v1/instances/${id}/firewall-rules`, {
31
+ method: "DELETE",
32
+ body: { cidr },
33
+ });
34
+ return json({ removed: true, id, cidr });
35
+ });
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerInstanceTools(server: McpServer): void;
@@ -0,0 +1,158 @@
1
+ import { z } from "zod";
2
+ import { api, json } from "../helpers/api.js";
3
+ import { waitForSettled } from "../helpers/wait.js";
4
+ import { makeProgressReporter } from "../helpers/progress.js";
5
+ export function registerInstanceTools(server) {
6
+ // ── Read / discovery ────────────────────────────────────────────────────
7
+ server.tool("list_instances", "List all GQLDB instances on your Ultipa Cloud account.", {}, async () => json(await api("/v1/instances")));
8
+ server.tool("list_deleted_instances", "List instances that have been deleted from the account (kept as soft-deleted tombstones for audit / recovery). These do NOT appear in `list_instances`.", {}, async () => json(await api("/v1/instances/deleted")));
9
+ server.tool("get_instance", "Get details for a single instance by ID. Does NOT include the admin password — use get_instance_credentials for that.", { id: z.string().describe("The instance ID") }, async ({ id }) => json(await api(`/v1/instances/${id}`)));
10
+ server.tool("get_instance_credentials", "Fetch the admin DB credentials (adminUser + adminPassword) for an instance. Requires the API key to have the `instances:credentials` scope). The call is audit-logged server-side.", { id: z.string().describe("The instance ID") }, async ({ id }) => json(await api(`/v1/instances/${id}/credentials`)));
11
+ server.tool("list_regions", "List all regions Ultipa Cloud supports. Each entry has `value` (the region code used by `create_instance`, e.g. `us-east-1`), `label` (human-readable name), `provider` (e.g. `aws`), and `managerUrl` (the region's GQLDB Manager URL). Useful as a pre-step for `create_instance` or to give the user the right Manager URL for an instance.", {}, async () => json(await api("/v1/regions")));
12
+ server.tool("list_instance_sizes", "List available GQLDB instance sizes (CPU, memory, storage, pricing). Optionally filter by tier or region.", {
13
+ tier: z
14
+ .enum(["free_trial", "standard", "enterprise"])
15
+ .optional()
16
+ .describe("Filter by tier"),
17
+ region: z
18
+ .string()
19
+ .optional()
20
+ .describe("Filter by region code, e.g. us-east-1"),
21
+ }, async ({ tier, region }) => {
22
+ const q = new URLSearchParams();
23
+ if (tier)
24
+ q.set("tier", tier);
25
+ if (region)
26
+ q.set("region", region);
27
+ const qs = q.toString();
28
+ return json(await api(`/v1/instance-sizes${qs ? `?${qs}` : ""}`));
29
+ });
30
+ server.tool("get_enterprise_status", "Check the account's enterprise-tier eligibility. Returns `{ hasActiveEnterprise, canCreateEnterprise }`. Only meaningful for accounts whose email has enterprise sizes assigned. Pre-check before `create_instance` with an `enterprise` size.", {}, async () => json(await api("/v1/instances/enterprise-status")));
31
+ server.tool("get_operations_lock", "Check whether instance operations are currently locked (Ultipa Cloud maintenance). Returns `{ locked }`. When `locked: true`, all write/destructive ops (create, pause, resume, restart, delete, etc.) will be rejected upstream. Useful as a pre-check before chaining state-change tools — if locked, tell the user to wait rather than triggering ops that will fail.", {}, async () => json(await api("/v1/instances/operations-lock")));
32
+ server.tool("get_trial_status", "Check the account's free-trial eligibility. Returns `{ trialStartsAt, trialEndsAt, hasActiveTrial, canCreateTrial }`. Call before `create_instance` with a `free_trial` size — if `canCreateTrial` is false (trial expired or one already running), creating will fail.", {}, async () => json(await api("/v1/instances/trial-status")));
33
+ server.tool("get_latest_version", "Return the latest available GQLDB version. Pair with `get_instance` to compare against an instance's current `version` before calling `upgrade_version` — saves a 409 'already on latest' from the server when there's nothing to upgrade.", {}, async () => json(await api("/v1/gqldb-versions/latest")));
34
+ // ── State changes ───────────────────────────────────────────────────────
35
+ server.tool("create_instance", "Provision a new GQLDB instance. Blocks until the instance is fully provisioned and running (typically 30–60s). Returns the final instance object with `adminUser` and `adminPassword` merged in. **The `POST /v1/instances` response is the ONE place the password is surfaced** — subsequent GETs strip it — so surface the password to the user immediately on return. No follow-up `get_instance_credentials` call is needed.", {
36
+ name: z.string().min(1).max(30).describe("Instance name (1–30 chars)"),
37
+ region: z
38
+ .string()
39
+ .describe("Region code, e.g. us-east-1. Use list_instance_sizes to see valid regions."),
40
+ sizeId: z.string().describe("Size ID from list_instance_sizes"),
41
+ }, async ({ name, region, sizeId }, extra) => {
42
+ const onProgress = makeProgressReporter(extra);
43
+ await onProgress?.("Submitting create request...", undefined);
44
+ // POST /v1/instances is the only place adminPassword is surfaced — capture
45
+ // it now and merge into the final return after waitForSettled (which polls
46
+ // GET /v1/instances/:id, and GET strips the password).
47
+ const created = (await api("/v1/instances", {
48
+ method: "POST",
49
+ body: { name, region, sizeId },
50
+ }));
51
+ try {
52
+ const settled = await waitForSettled(created._id, "running", {
53
+ onProgress,
54
+ });
55
+ return json({ ...settled, adminPassword: created.adminPassword });
56
+ }
57
+ catch (e) {
58
+ throw new Error(`Instance ${created._id} WAS created but waiting for "running" failed: ${e?.message ?? e}. Do NOT call create_instance again — that would provision a duplicate. Initial adminPassword from the create response: "${created.adminPassword ?? "<not in response>"}" — surface it to the user before retrying anything. Call wait_for_instance_status(id="${created._id}") to keep waiting, or get_instance(id="${created._id}") to check the current state.`);
59
+ }
60
+ });
61
+ server.tool("rename_instance", "Rename an instance (display name only — does not affect host, port, credentials, or any client connections). Synchronous: returns the updated instance immediately.", {
62
+ id: z.string().describe("The instance ID"),
63
+ name: z.string().min(1).max(30).describe("New name (1–30 chars)"),
64
+ }, async ({ id, name }) => json(await api(`/v1/instances/${id}`, {
65
+ method: "PATCH",
66
+ body: { name },
67
+ })));
68
+ server.tool("pause_instance", "Pause a running GQLDB instance. Blocks until the pause completes and the instance has fully settled in 'paused' (typically ~60s). Stops compute billing while paused (storage still billed).", { id: z.string().describe("The instance ID") }, async ({ id }, extra) => {
69
+ const onProgress = makeProgressReporter(extra);
70
+ await api(`/v1/instances/${id}/pause`, { method: "POST" });
71
+ return json(await waitForSettled(id, "paused", { onProgress }));
72
+ });
73
+ server.tool("resume_instance", "Resume a paused GQLDB instance. Blocks until the resume completes and the instance is fully 'running' (typically 60–120s). Note: during resume, `status` stays 'paused' the whole time and only `progressStep` changes — that's expected.", { id: z.string().describe("The instance ID") }, async ({ id }, extra) => {
74
+ const onProgress = makeProgressReporter(extra);
75
+ await api(`/v1/instances/${id}/resume`, { method: "POST" });
76
+ return json(await waitForSettled(id, "running", { onProgress }));
77
+ });
78
+ server.tool("restart_instance", "Restart a GQLDB instance. Blocks until the restart completes and the instance is back to 'running' (typically ~60s). Note: `status` stays 'running' for the whole restart — only `progressStep` changes.", { id: z.string().describe("The instance ID") }, async ({ id }, extra) => {
79
+ const onProgress = makeProgressReporter(extra);
80
+ await api(`/v1/instances/${id}/restart`, { method: "POST" });
81
+ return json(await waitForSettled(id, "running", { onProgress }));
82
+ });
83
+ server.tool("upgrade_version", "Upgrade a GQLDB instance to the latest available version. Blocks until the upgrade completes and the instance is back to 'running' (can take several minutes; default timeout 5 min). Errors out with 409 if already on the latest version — call `get_latest_version` first and compare against the instance's current `version` if you want to avoid that.", { id: z.string().describe("The instance ID") }, async ({ id }, extra) => {
84
+ const onProgress = makeProgressReporter(extra);
85
+ await api(`/v1/instances/${id}/upgrade`, { method: "POST" });
86
+ return json(await waitForSettled(id, "running", {
87
+ onProgress,
88
+ timeoutMs: 300_000,
89
+ }));
90
+ });
91
+ server.tool("delete_instance", "Delete an instance. Blocks until deletion fully completes upstream (status reaches 'deleted' or the instance is gone — typically 30–60s). Requires the instance name as a confirmation arg — must exactly match the current instance name, or the call is rejected without contacting the server.", {
92
+ id: z.string().describe("The instance ID"),
93
+ confirmName: z
94
+ .string()
95
+ .describe("Must exactly match the target instance's current name"),
96
+ }, async ({ id, confirmName }, extra) => {
97
+ const onProgress = makeProgressReporter(extra);
98
+ const inst = (await api(`/v1/instances/${id}`));
99
+ if (inst?.name !== confirmName) {
100
+ throw new Error(`confirmName mismatch: instance ${id} is named "${inst?.name}", but confirmName was "${confirmName}". Refusing to delete.`);
101
+ }
102
+ await api(`/v1/instances/${id}`, { method: "DELETE" });
103
+ await waitForSettled(id, "deleted", { onProgress });
104
+ return json({ deleted: true, id, name: confirmName });
105
+ });
106
+ server.tool("reset_admin_password", "Rotate the admin DB password for an instance and return the new value. Uses the `instances:write` scope. WARNING: any existing apps / drivers / sessions using the old password will be broken until reconfigured. Only call this when the user explicitly asks to reset/rotate the password — do not call it as an automatic fallback for `get_instance_credentials`.", {
107
+ id: z.string().describe("The instance ID"),
108
+ password: z
109
+ .string()
110
+ .min(6)
111
+ .max(128)
112
+ .optional()
113
+ .describe("Optional new password (6–128 chars). If omitted, the server generates one."),
114
+ }, async ({ id, password }) => json(await api(`/v1/instances/${id}/reset-password`, {
115
+ method: "POST",
116
+ body: password === undefined ? {} : { password },
117
+ })));
118
+ server.tool("set_log_level", "Set the GQLDB log level on an instance. Blocks until the change is applied (typically a few seconds). `status` stays 'running' throughout; only `progressStep` changes.", {
119
+ id: z.string().describe("The instance ID"),
120
+ level: z
121
+ .enum(["debug", "info", "warn", "error"])
122
+ .describe("New log level"),
123
+ }, async ({ id, level }, extra) => {
124
+ const onProgress = makeProgressReporter(extra);
125
+ await api(`/v1/instances/${id}/log-level`, {
126
+ method: "POST",
127
+ body: { level },
128
+ });
129
+ return json(await waitForSettled(id, "running", { onProgress }));
130
+ });
131
+ // ── Recovery / explicit polling (rarely needed directly) ────────────────
132
+ server.tool("wait_for_instance_status", "Block until an instance settles into the target status (default 'running'). NOTE: you usually do NOT need to call this directly — `create_instance`, `pause_instance`, `resume_instance`, `restart_instance`, `upgrade_version`, `set_log_level`, and `delete_instance` all auto-wait internally. Use this only for explicit/manual polling (e.g. recovering from a half-known state, or waiting on an instance someone else triggered). An instance is 'in transition' whenever `progressStep` is non-empty OR `status` is one of `provisioning`/`upgrading`/`deleting`. Throws on `error` / `suspended` / `deleted`, on settling on any other non-target status, or on timeout.", {
133
+ id: z.string().describe("The instance ID"),
134
+ target: z
135
+ .enum(["running", "paused"])
136
+ .default("running")
137
+ .describe("Target status. Default 'running'. Use 'paused' after pause_instance."),
138
+ timeoutMs: z
139
+ .number()
140
+ .int()
141
+ .positive()
142
+ .default(180_000)
143
+ .describe("Max time to wait, in milliseconds. Default 180000 (3 min). Resume / restart of larger instances can take 60–120s."),
144
+ pollIntervalMs: z
145
+ .number()
146
+ .int()
147
+ .positive()
148
+ .default(3000)
149
+ .describe("Polling interval, in milliseconds. Default 3000."),
150
+ }, async ({ id, target, timeoutMs, pollIntervalMs }, extra) => {
151
+ const onProgress = makeProgressReporter(extra);
152
+ return json(await waitForSettled(id, target, {
153
+ timeoutMs,
154
+ pollIntervalMs,
155
+ onProgress,
156
+ }));
157
+ });
158
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerLogTools(server: McpServer): void;
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { api, json } from "../helpers/api.js";
3
+ export function registerLogTools(server) {
4
+ server.tool("get_instance_logs", "Fetch recent container logs from a GQLDB instance. Default 100 lines, max 1000.", {
5
+ id: z.string().describe("The instance ID"),
6
+ limit: z
7
+ .number()
8
+ .int()
9
+ .min(1)
10
+ .max(1000)
11
+ .default(100)
12
+ .describe("Max number of log lines to return (1–1000). Default 100."),
13
+ }, async ({ id, limit }) => json(await api(`/v1/instances/${id}/logs?limit=${limit}`)));
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerMetricsTools(server: McpServer): void;
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { api, json } from "../helpers/api.js";
3
+ export function registerMetricsTools(server) {
4
+ server.tool("get_live_metrics", "Current snapshot of an instance's metrics (CPU, memory, disk, network). Single point-in-time reading; use `get_metrics_history` for a time series.", { id: z.string().describe("The instance ID") }, async ({ id }) => json(await api(`/v1/instances/${id}/metrics`)));
5
+ server.tool("get_metrics_history", "Historical metrics for an instance over the last N minutes. Default 60, max 20160 (14 days).", {
6
+ id: z.string().describe("The instance ID"),
7
+ range: z
8
+ .number()
9
+ .int()
10
+ .min(1)
11
+ .max(20160)
12
+ .default(60)
13
+ .describe("Lookback window in minutes (1–20160). Default 60."),
14
+ }, async ({ id, range }) => json(await api(`/v1/instances/${id}/metrics/history?range=${range}`)));
15
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "ultipa-mcp",
3
+ "version": "1.0.1",
4
+ "description": "Model Context Protocol server for Ultipa Cloud and any self-managed Ultipa GQLDB instance. Operate instances, run GQL, manage backups from any MCP client (Claude Desktop, Cursor, etc.).",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "ultipa-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "start": "tsx src/index.ts",
20
+ "dev": "tsx watch src/index.ts",
21
+ "typecheck": "tsc --noEmit",
22
+ "format": "prettier --write \"src/**/*.ts\"",
23
+ "format:check": "prettier --check \"src/**/*.ts\"",
24
+ "clean": "rm -rf dist",
25
+ "build": "npm run clean && tsc && chmod +x dist/index.js",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "mcp",
30
+ "model-context-protocol",
31
+ "ultipa",
32
+ "gqldb",
33
+ "graph-database",
34
+ "gql",
35
+ "claude",
36
+ "cursor"
37
+ ],
38
+ "author": "Ultipa",
39
+ "license": "ISC",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/ultipa/ultipa-mcp.git"
43
+ },
44
+ "homepage": "https://github.com/ultipa/ultipa-mcp#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/ultipa/ultipa-mcp/issues"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.29.0",
50
+ "@ultipa-graph/ultipa-driver": "^6.0.0",
51
+ "zod": "^4.4.3"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.10.0",
55
+ "prettier": "^3.4.2",
56
+ "tsx": "^4.22.4",
57
+ "typescript": "^5.6.3"
58
+ }
59
+ }