nxvet-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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NerveX Technologies
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,119 @@
1
+ # nxvet-mcp
2
+
3
+ [MCP](https://modelcontextprotocol.io) server for the [NxVET API](https://api.nx.vet).
4
+ Lets Claude, Cursor, and other MCP clients work with NxVET data: consultation labels with
5
+ transcripts and SOAP notes, NxHub conversations and device timelines, webhooks, and API keys.
6
+
7
+ Two ways to use it, same tools either way:
8
+
9
+ - **Hosted (recommended)**: `https://mcp.nx.vet/mcp` — Streamable HTTP, no install.
10
+ - **Local (npm)**: `npx nxvet-mcp` — stdio transport, for clients without remote-header support
11
+ (e.g. Claude Desktop before OAuth lands).
12
+
13
+ ## How it works
14
+
15
+ - **Hosted transport**: Streamable HTTP at `POST /mcp`, fully stateless (no sessions) — safe behind a load balancer.
16
+ - **Auth**: hosted — each request carries the caller's NxVET API key as `Authorization: Bearer nxvet_sk_...`;
17
+ local — the key comes from the `NXVET_API_KEY` env var. Either way the key is forwarded unchanged to
18
+ `https://app.nx.vet`; the MCP server stores nothing.
19
+ - Keys are created at [app.nx.vet/integrations](https://app.nx.vet/integrations) → API Keys.
20
+
21
+ ## Connecting a client
22
+
23
+ Claude Code:
24
+
25
+ ```bash
26
+ claude mcp add nxvet --transport http https://mcp.nx.vet/mcp \
27
+ --header "Authorization: Bearer nxvet_sk_YOUR_KEY"
28
+ ```
29
+
30
+ `.mcp.json` / other clients:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "nxvet": {
36
+ "type": "http",
37
+ "url": "https://mcp.nx.vet/mcp",
38
+ "headers": { "Authorization": "Bearer nxvet_sk_YOUR_KEY" }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ > claude.ai custom connectors currently require OAuth or unauthenticated URLs; header-based
45
+ > API-key auth works in Claude Code, Cursor, and the MCP Inspector. OAuth support is a future step.
46
+
47
+ Claude Desktop (`claude_desktop_config.json`) or any stdio-only client — uses the npm package:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "nxvet": {
53
+ "command": "npx",
54
+ "args": ["-y", "nxvet-mcp"],
55
+ "env": { "NXVET_API_KEY": "nxvet_sk_YOUR_KEY" }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ OpenAI Codex CLI (`~/.codex/config.toml`) — also via the npm package:
62
+
63
+ ```toml
64
+ [mcp_servers.nxvet]
65
+ command = "npx"
66
+ args = ["-y", "nxvet-mcp"]
67
+ env = { "NXVET_API_KEY" = "nxvet_sk_YOUR_KEY" }
68
+ ```
69
+
70
+ Claude Code can use the local package too (instead of the hosted URL), e.g. for offline dev against staging:
71
+
72
+ ```bash
73
+ claude mcp add nxvet -e NXVET_API_KEY=nxvet_sk_YOUR_KEY -- npx -y nxvet-mcp
74
+ ```
75
+
76
+ ## Tools
77
+
78
+ | Area | Tools |
79
+ |---|---|
80
+ | Identity | `get_identity` |
81
+ | Labels & transcripts | `list_labels`, `get_label`, `assign_patient_to_label`, `aggregate_labels`, `regenerate_label`, `list_medical_templates` |
82
+ | NxHub conversations | `list_conversations`, `get_conversation`, `get_device_timeline`, `preview_conversation`, `create_conversation`, `complete_conversation` |
83
+ | Webhooks | `list_webhooks`, `create_webhook`, `delete_webhook`, `test_webhook`, `enable_webhook`, `get_webhook_deliveries` |
84
+ | API keys | `list_api_keys`, `create_api_key`, `revoke_api_key` |
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ npm install
90
+ npm run dev # tsx watch on :3000
91
+ npm run build # tsc → dist/
92
+ npm start
93
+ ```
94
+
95
+ Environment variables:
96
+
97
+ | Variable | Default | Purpose |
98
+ |---|---|---|
99
+ | `PORT` | `3000` | HTTP listen port |
100
+ | `NXVET_API_BASE` | `https://app.nx.vet` | Upstream NxVET API (point at staging for testing) |
101
+
102
+ Smoke test with the MCP Inspector:
103
+
104
+ ```bash
105
+ npx @modelcontextprotocol/inspector
106
+ # Transport: Streamable HTTP, URL: http://localhost:3000/mcp
107
+ # Add header Authorization: Bearer nxvet_sk_...
108
+ ```
109
+
110
+ ## Deployment
111
+
112
+ Container listens on `:3000` with `GET /health` for health checks — ready for App Runner / ECS:
113
+
114
+ ```bash
115
+ docker build -t nxvet-mcp .
116
+ docker run -p 3000:3000 nxvet-mcp
117
+ ```
118
+
119
+ (For ECS from Apple Silicon, remember `docker buildx build --platform linux/amd64`.)
package/dist/client.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Thin HTTP client for the NxVET REST API (https://app.nx.vet).
3
+ * One instance per MCP request; the caller's API key is forwarded verbatim.
4
+ */
5
+ export class NxVetApiError extends Error {
6
+ status;
7
+ responseBody;
8
+ constructor(status, responseBody, method, path) {
9
+ super(`NxVET API ${method} ${path} failed with status ${status}`);
10
+ this.status = status;
11
+ this.responseBody = responseBody;
12
+ this.name = "NxVetApiError";
13
+ }
14
+ }
15
+ export class NxVetApiClient {
16
+ apiKey;
17
+ baseUrl;
18
+ constructor(apiKey, baseUrl = process.env.NXVET_API_BASE ?? "https://app.nx.vet") {
19
+ this.apiKey = apiKey;
20
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
21
+ }
22
+ async get(path, query) {
23
+ return this.request("GET", path, { query });
24
+ }
25
+ async post(path, options = {}) {
26
+ return this.request("POST", path, options);
27
+ }
28
+ async delete(path) {
29
+ return this.request("DELETE", path, {});
30
+ }
31
+ async request(method, path, options) {
32
+ const url = new URL(this.baseUrl + path);
33
+ for (const [key, value] of Object.entries(options.query ?? {})) {
34
+ if (value === undefined)
35
+ continue;
36
+ if (Array.isArray(value)) {
37
+ for (const item of value)
38
+ url.searchParams.append(key, item);
39
+ }
40
+ else {
41
+ url.searchParams.set(key, String(value));
42
+ }
43
+ }
44
+ const response = await fetch(url, {
45
+ method,
46
+ headers: {
47
+ Authorization: `Bearer ${this.apiKey}`,
48
+ Accept: "application/json",
49
+ ...(options.body !== undefined ? { "Content-Type": "application/json" } : {}),
50
+ },
51
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
52
+ });
53
+ const text = await response.text();
54
+ let body = null;
55
+ if (text.length > 0) {
56
+ try {
57
+ body = JSON.parse(text);
58
+ }
59
+ catch {
60
+ body = text;
61
+ }
62
+ }
63
+ const allowed = options.allowStatuses ?? [];
64
+ if (!response.ok && !allowed.includes(response.status)) {
65
+ throw new NxVetApiError(response.status, body, method, path);
66
+ }
67
+ return { status: response.status, headers: response.headers, body };
68
+ }
69
+ /**
70
+ * POST /api/labels/{id}/lambda is fire-and-forget: the API gateway often
71
+ * times out (504) while the regeneration job keeps running. Both 200 and
72
+ * 504 mean "triggered" — never retry on 504.
73
+ */
74
+ async postLambdaRegenerate(labelId, query) {
75
+ return this.request("POST", `/api/labels/${labelId}/lambda`, {
76
+ query,
77
+ allowStatuses: [504],
78
+ });
79
+ }
80
+ }
81
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/format.js ADDED
@@ -0,0 +1,42 @@
1
+ import { NxVetApiError } from "./client.js";
2
+ /** Cap tool output so a huge transcript list can't blow out the client context. */
3
+ const MAX_RESULT_CHARS = 100_000;
4
+ export function jsonResult(data, note) {
5
+ let text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
6
+ if (text.length > MAX_RESULT_CHARS) {
7
+ text =
8
+ text.slice(0, MAX_RESULT_CHARS) +
9
+ `\n… [truncated at ${MAX_RESULT_CHARS} chars — narrow the query (smaller limit/pageSize or a tighter time range) to see full results]`;
10
+ }
11
+ if (note) {
12
+ text = `${note}\n\n${text}`;
13
+ }
14
+ return { content: [{ type: "text", text }] };
15
+ }
16
+ export function errorResult(error) {
17
+ let text;
18
+ if (error instanceof NxVetApiError) {
19
+ const hints = {
20
+ 401: "The API key is invalid, expired, or revoked. Verify it with the get_identity tool.",
21
+ 403: "The key is valid but lacks permission for this organization or operation.",
22
+ 404: "Resource not found — check the ID.",
23
+ 429: "Rate limited — wait and retry with exponential backoff.",
24
+ };
25
+ const hint = hints[error.status] ? ` ${hints[error.status]}` : "";
26
+ text = `${error.message}.${hint}\nResponse body: ${JSON.stringify(error.responseBody)}`;
27
+ }
28
+ else {
29
+ text = `Request failed: ${error instanceof Error ? error.message : String(error)}`;
30
+ }
31
+ return { isError: true, content: [{ type: "text", text }] };
32
+ }
33
+ /** Wrap a tool handler so NxVET API errors surface as tool errors, not protocol errors. */
34
+ export async function run(fn) {
35
+ try {
36
+ return await fn();
37
+ }
38
+ catch (error) {
39
+ return errorResult(error);
40
+ }
41
+ }
42
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,mFAAmF;AACnF,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAEjC,MAAM,UAAU,UAAU,CAAC,IAAa,EAAE,IAAa;IACrD,IAAI,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC3E,IAAI,IAAI,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;QACnC,IAAI;YACF,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC;gBAC/B,qBAAqB,gBAAgB,iGAAiG,CAAC;IAC3I,CAAC;IACD,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,GAAG,GAAG,IAAI,OAAO,IAAI,EAAE,CAAC;IAC9B,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,IAAY,CAAC;IACjB,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;QACnC,MAAM,KAAK,GAA2B;YACpC,GAAG,EAAE,oFAAoF;YACzF,GAAG,EAAE,2EAA2E;YAChF,GAAG,EAAE,oCAAoC;YACzC,GAAG,EAAE,yDAAyD;SAC/D,CAAC;QACF,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClE,IAAI,GAAG,GAAG,KAAK,CAAC,OAAO,IAAI,IAAI,oBAAoB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC;IAC1F,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,mBAAmB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IACrF,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,EAAiC;IACzD,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,EAAE,CAAC;IACpB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,70 @@
1
+ import express from "express";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { buildServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
4
+ const PORT = Number(process.env.PORT ?? 3000);
5
+ const app = express();
6
+ app.disable("x-powered-by");
7
+ app.use(express.json({ limit: "1mb" }));
8
+ app.get("/health", (_req, res) => {
9
+ res.json({ status: "ok", name: SERVER_NAME, version: SERVER_VERSION });
10
+ });
11
+ /**
12
+ * Stateless Streamable HTTP MCP endpoint.
13
+ * Every POST carries the full JSON-RPC message plus the caller's NxVET API key
14
+ * as `Authorization: Bearer nxvet_sk_...`; a fresh server + transport pair is
15
+ * created per request, so any instance behind a load balancer can serve it.
16
+ */
17
+ app.post("/mcp", async (req, res) => {
18
+ const auth = req.headers.authorization;
19
+ if (!auth || !auth.startsWith("Bearer ")) {
20
+ res
21
+ .status(401)
22
+ .set("WWW-Authenticate", 'Bearer realm="nxvet-mcp"')
23
+ .json({
24
+ jsonrpc: "2.0",
25
+ error: {
26
+ code: -32001,
27
+ message: "Missing API key. Send 'Authorization: Bearer nxvet_sk_...' — create a key at https://app.nx.vet/integrations",
28
+ },
29
+ id: null,
30
+ });
31
+ return;
32
+ }
33
+ const apiKey = auth.slice("Bearer ".length).trim();
34
+ const server = buildServer(apiKey);
35
+ const transport = new StreamableHTTPServerTransport({
36
+ sessionIdGenerator: undefined, // stateless mode
37
+ });
38
+ res.on("close", () => {
39
+ transport.close();
40
+ server.close();
41
+ });
42
+ try {
43
+ await server.connect(transport);
44
+ await transport.handleRequest(req, res, req.body);
45
+ }
46
+ catch (error) {
47
+ console.error("MCP request failed:", error);
48
+ if (!res.headersSent) {
49
+ res.status(500).json({
50
+ jsonrpc: "2.0",
51
+ error: { code: -32603, message: "Internal server error" },
52
+ id: null,
53
+ });
54
+ }
55
+ }
56
+ });
57
+ // Stateless server: no SSE resumption stream, no sessions to delete.
58
+ const methodNotAllowed = (_req, res) => {
59
+ res.status(405).json({
60
+ jsonrpc: "2.0",
61
+ error: { code: -32000, message: "Method not allowed. This server is stateless — use POST /mcp." },
62
+ id: null,
63
+ });
64
+ };
65
+ app.get("/mcp", methodNotAllowed);
66
+ app.delete("/mcp", methodNotAllowed);
67
+ app.listen(PORT, () => {
68
+ console.log(`${SERVER_NAME} v${SERVER_VERSION} listening on :${PORT} (POST /mcp)`);
69
+ });
70
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEvE,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AAE9C,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AACtB,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;AAC5B,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAExC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAC/B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;AACzE,CAAC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IAClC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACvC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACzC,GAAG;aACA,MAAM,CAAC,GAAG,CAAC;aACX,GAAG,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;aACnD,IAAI,CAAC;YACJ,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,CAAC,KAAK;gBACZ,OAAO,EACL,8GAA8G;aACjH;YACD,EAAE,EAAE,IAAI;SACT,CAAC,CAAC;QACL,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IACnD,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC;QAClD,kBAAkB,EAAE,SAAS,EAAE,iBAAiB;KACjD,CAAC,CAAC;IAEH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QACnB,SAAS,CAAC,KAAK,EAAE,CAAC;QAClB,MAAM,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAChC,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE;gBACzD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;QACL,CAAC;IACH,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,qEAAqE;AACrE,MAAM,gBAAgB,GAAG,CAAC,IAAqB,EAAE,GAAqB,EAAE,EAAE;IACxE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,+DAA+D,EAAE;QACjG,EAAE,EAAE,IAAI;KACT,CAAC,CAAC;AACL,CAAC,CAAC;AACF,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAClC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAErC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,KAAK,cAAc,kBAAkB,IAAI,cAAc,CAAC,CAAC;AACrF,CAAC,CAAC,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1,30 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { NxVetApiClient } from "./client.js";
3
+ import { registerIdentityTools } from "./tools/identity.js";
4
+ import { registerLabelTools } from "./tools/labels.js";
5
+ import { registerConversationTools } from "./tools/conversations.js";
6
+ import { registerWebhookTools } from "./tools/webhooks.js";
7
+ import { registerApiKeyTools } from "./tools/apiKeys.js";
8
+ export const SERVER_NAME = "nxvet-mcp";
9
+ export const SERVER_VERSION = "0.1.0";
10
+ /**
11
+ * Build a fresh MCP server bound to one caller's API key.
12
+ * The server is stateless — one instance is created per HTTP request and
13
+ * every NxVET API call forwards the caller's Bearer key unchanged.
14
+ */
15
+ export function buildServer(apiKey) {
16
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, {
17
+ instructions: "Tools for the NxVET veterinary platform API (app.nx.vet): consultation labels with transcripts and " +
18
+ "SOAP notes, NxHub ambient-recording conversations and device timelines, webhook management, and API keys. " +
19
+ "Start with get_identity to discover the caller's organizationId. " +
20
+ "All *Ms timestamps are Unix epoch milliseconds; label metadata and SOAP content are JSON strings that need parsing.",
21
+ });
22
+ const client = new NxVetApiClient(apiKey);
23
+ registerIdentityTools(server, client);
24
+ registerLabelTools(server, client);
25
+ registerConversationTools(server, client);
26
+ registerWebhookTools(server, client);
27
+ registerApiKeyTools(server, client);
28
+ return server;
29
+ }
30
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC;AACvC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,EAC9C;QACE,YAAY,EACV,qGAAqG;YACrG,4GAA4G;YAC5G,mEAAmE;YACnE,qHAAqH;KACxH,CACF,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;IAC1C,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,yBAAyB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEpC,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/stdio.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Local stdio entry point for the npm package (npx nxvet-mcp).
4
+ * Same tools as the hosted server at mcp.nx.vet; the API key comes from
5
+ * the NXVET_API_KEY environment variable instead of an Authorization header.
6
+ * Nothing may write to stdout except the MCP transport.
7
+ */
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { buildServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
10
+ const apiKey = process.env.NXVET_API_KEY;
11
+ if (!apiKey) {
12
+ console.error("Error: the NXVET_API_KEY environment variable is required.\n" +
13
+ "Create an API key at https://app.nx.vet/integrations (API Keys tab), then configure:\n" +
14
+ ' "env": { "NXVET_API_KEY": "nxvet_sk_..." }');
15
+ process.exit(1);
16
+ }
17
+ const server = buildServer(apiKey);
18
+ await server.connect(new StdioServerTransport());
19
+ console.error(`${SERVER_NAME} v${SERVER_VERSION} running on stdio`);
20
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEvE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;AACzC,IAAI,CAAC,MAAM,EAAE,CAAC;IACZ,OAAO,CAAC,KAAK,CACX,8DAA8D;QAC5D,wFAAwF;QACxF,8CAA8C,CACjD,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;AACnC,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;AACjD,OAAO,CAAC,KAAK,CAAC,GAAG,WAAW,KAAK,cAAc,mBAAmB,CAAC,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { jsonResult, run } from "../format.js";
3
+ export function registerApiKeyTools(server, client) {
4
+ server.registerTool("list_api_keys", {
5
+ title: "List API keys",
6
+ description: "List all API keys created by the current user (name, prefix, creation/last-used/expiry, revoked flag). Full key values are never returned.",
7
+ inputSchema: {},
8
+ }, async () => run(async () => jsonResult((await client.get("/api/api-keys")).body)));
9
+ server.registerTool("create_api_key", {
10
+ title: "Create API key",
11
+ description: "Create a new NxVET API key. IMPORTANT: the full `key` value in the response is shown ONLY ONCE — the user must store it immediately.",
12
+ inputSchema: {
13
+ name: z.string().describe("Human-readable key name, e.g. 'Production Integration'"),
14
+ organizationId: z.string().uuid().describe("Organization the key is scoped to"),
15
+ expiresAt: z
16
+ .string()
17
+ .datetime()
18
+ .optional()
19
+ .describe("Optional ISO 8601 expiry, e.g. 2027-01-01T00:00:00Z"),
20
+ },
21
+ }, async (body) => run(async () => jsonResult((await client.post("/api/api-keys", { body })).body, "Save the `key` value now — it cannot be retrieved again.")));
22
+ server.registerTool("revoke_api_key", {
23
+ title: "Revoke API key",
24
+ description: "Revoke an API key immediately and irreversibly. Subsequent requests with that key receive 401. " +
25
+ "Do not revoke the key currently authenticating this MCP session unless the user explicitly asks.",
26
+ inputSchema: {
27
+ apiKeyId: z.string().uuid().describe("API key ID (from list_api_keys), not the key value"),
28
+ },
29
+ }, async ({ apiKeyId }) => run(async () => {
30
+ await client.delete(`/api/api-keys/${apiKeyId}`);
31
+ return jsonResult({ revoked: true, apiKeyId });
32
+ }));
33
+ }
34
+ //# sourceMappingURL=apiKeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiKeys.js","sourceRoot":"","sources":["../../src/tools/apiKeys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,UAAU,mBAAmB,CAAC,MAAiB,EAAE,MAAsB;IAC3E,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;QACE,KAAK,EAAE,eAAe;QACtB,WAAW,EACT,4IAA4I;QAC9I,WAAW,EAAE,EAAE;KAChB,EACD,KAAK,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAClF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,KAAK,EAAE,gBAAgB;QACvB,WAAW,EACT,sIAAsI;QACxI,WAAW,EAAE;YACX,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;YACnF,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC;YAC/E,SAAS,EAAE,CAAC;iBACT,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,EAAE;iBACV,QAAQ,CAAC,qDAAqD,CAAC;SACnE;KACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CACb,GAAG,CAAC,KAAK,IAAI,EAAE,CACb,UAAU,CACR,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,EACnD,0DAA0D,CAC3D,CACF,CACJ,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,KAAK,EAAE,gBAAgB;QACvB,WAAW,EACT,iGAAiG;YACjG,kGAAkG;QACpG,WAAW,EAAE;YACX,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,oDAAoD,CAAC;SAC3F;KACF,EACD,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CACrB,GAAG,CAAC,KAAK,IAAI,EAAE;QACb,MAAM,MAAM,CAAC,MAAM,CAAC,iBAAiB,QAAQ,EAAE,CAAC,CAAC;QACjD,OAAO,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC,CACL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ import { jsonResult, run } from "../format.js";
3
+ // Device and conversation IDs are interpolated into URL paths — restrict them
4
+ // to safe characters (defense-in-depth alongside encodeURIComponent below).
5
+ const SAFE_ID = /^[A-Za-z0-9._~-]+$/;
6
+ const deviceIdSchema = z
7
+ .string()
8
+ .regex(SAFE_ID, "deviceId may only contain letters, digits, and . _ ~ -")
9
+ .describe("Device ID (e.g. nxhub-B7FA7D4B1D8D28CF)");
10
+ const conversationIdSchema = z
11
+ .string()
12
+ .regex(SAFE_ID, "conversationId may only contain letters, digits, and . _ ~ -")
13
+ .describe("Conversation ID");
14
+ const enc = encodeURIComponent;
15
+ export function registerConversationTools(server, client) {
16
+ server.registerTool("list_conversations", {
17
+ title: "List NxHub conversations",
18
+ description: "List NxHub conversations for an organization with token-based pagination " +
19
+ "(pass nextPageToken from the previous response as pageToken; hasMore indicates more pages). " +
20
+ "Timestamps (startMinuteMs, endMinuteMs, createdAt, …) are Unix epoch milliseconds. " +
21
+ "An UPLOADED conversation has an nxvetLabelId linking to its label (use get_label for the SOAP note).",
22
+ inputSchema: {
23
+ organizationId: z.string().uuid().describe("Organization UUID (discover via get_identity)"),
24
+ deviceId: deviceIdSchema.optional().describe("Filter by device ID"),
25
+ status: z
26
+ .enum(["ACTIVE", "COMPLETED", "PROCESSING", "UPLOADED", "FAILED", "FILTERED"])
27
+ .optional()
28
+ .describe("Filter by conversation status"),
29
+ pageSize: z.number().int().min(1).max(100).default(20),
30
+ pageToken: z.string().optional().describe("nextPageToken from the previous page"),
31
+ },
32
+ }, async (args) => run(async () => jsonResult((await client.get("/api/nxhub/conversations", args)).body)));
33
+ server.registerTool("get_conversation", {
34
+ title: "Get NxHub conversation details",
35
+ description: "Get one NxHub conversation, including transcript text, speech/silence stats, status, and speaker matches when available.",
36
+ inputSchema: {
37
+ deviceId: deviceIdSchema,
38
+ conversationId: conversationIdSchema,
39
+ },
40
+ }, async ({ deviceId, conversationId }) => run(async () => jsonResult((await client.get(`/api/nxhub/conversations/${enc(deviceId)}/${enc(conversationId)}`)).body)));
41
+ server.registerTool("get_device_timeline", {
42
+ title: "Get device timeline",
43
+ description: "Minute-by-minute speech-activity timeline for a device, including per-minute transcript text where available. " +
44
+ "Time range must not exceed 6 hours. startMs/endMs are Unix epoch milliseconds.",
45
+ inputSchema: {
46
+ deviceId: deviceIdSchema,
47
+ startMs: z.number().int().describe("Range start, epoch milliseconds"),
48
+ endMs: z.number().int().describe("Range end, epoch milliseconds (max 6h after startMs)"),
49
+ },
50
+ }, async ({ deviceId, startMs, endMs }) => run(async () => jsonResult((await client.get(`/api/nxhub/conversations/timeline/${enc(deviceId)}`, {
51
+ startMs,
52
+ endMs,
53
+ })).body)));
54
+ server.registerTool("preview_conversation", {
55
+ title: "Preview conversation creation",
56
+ description: "Preview the speech/silence breakdown for a device time range before creating a conversation from it. " +
57
+ "Time range must not exceed 90 minutes.",
58
+ inputSchema: {
59
+ deviceId: deviceIdSchema,
60
+ startMs: z.number().int().describe("Range start, epoch milliseconds"),
61
+ endMs: z.number().int().describe("Range end, epoch milliseconds (max 90 min after startMs)"),
62
+ },
63
+ }, async (args) => run(async () => jsonResult((await client.get("/api/nxhub/conversations/create/preview", args)).body)));
64
+ server.registerTool("create_conversation", {
65
+ title: "Create conversation from time range",
66
+ description: "Create a new NxHub conversation from a device time range (max 90 minutes). " +
67
+ "Use preview_conversation first to check the range contains speech. " +
68
+ "Creation queues transcript processing; the conversation moves through COMPLETED → PROCESSING → UPLOADED.",
69
+ inputSchema: {
70
+ deviceId: deviceIdSchema,
71
+ startMs: z.number().int().describe("Range start, epoch milliseconds"),
72
+ endMs: z.number().int().describe("Range end, epoch milliseconds (max 90 min after startMs)"),
73
+ },
74
+ }, async (args) => run(async () => jsonResult((await client.post("/api/nxhub/conversations/create", { body: args })).body)));
75
+ server.registerTool("complete_conversation", {
76
+ title: "Force-complete active conversation",
77
+ description: "Mark an ACTIVE NxHub conversation as completed, triggering transcript processing immediately " +
78
+ "instead of waiting for the automatic silence-based completion.",
79
+ inputSchema: {
80
+ deviceId: deviceIdSchema,
81
+ conversationId: conversationIdSchema,
82
+ },
83
+ }, async ({ deviceId, conversationId }) => run(async () => jsonResult((await client.post(`/api/nxhub/conversations/${enc(deviceId)}/${enc(conversationId)}/complete`))
84
+ .body)));
85
+ }
86
+ //# sourceMappingURL=conversations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversations.js","sourceRoot":"","sources":["../../src/tools/conversations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { jsonResult, run } from "../format.js";
2
+ export function registerIdentityTools(server, client) {
3
+ server.registerTool("get_identity", {
4
+ title: "Get current identity",
5
+ description: "Return the caller's identity and organization context (userId, email, organizationId, organizationName, API key metadata). " +
6
+ "Call this FIRST to discover the organizationId required by most other tools and to verify the API key works.",
7
+ inputSchema: {},
8
+ }, async () => run(async () => jsonResult((await client.get("/api/auth/me")).body)));
9
+ }
10
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/tools/identity.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,UAAU,qBAAqB,CAAC,MAAiB,EAAE,MAAsB;IAC7E,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,6HAA6H;YAC7H,8GAA8G;QAChH,WAAW,EAAE,EAAE;KAChB,EACD,KAAK,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CACjF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ import { jsonResult, run } from "../format.js";
3
+ const LABEL_TYPES = [
4
+ "CombinedClinicConv",
5
+ "ClinicConversation",
6
+ "DictationAudio",
7
+ "AudioButtonRecording",
8
+ "NxMIC",
9
+ "NxHubBatch",
10
+ "DocumentSoap",
11
+ "PhoneCallAudio",
12
+ "ButtonRecording",
13
+ "Temperature",
14
+ "AggregateLabel",
15
+ "ClinicConversationChunk",
16
+ ];
17
+ export function registerLabelTools(server, client) {
18
+ server.registerTool("list_labels", {
19
+ title: "List labels (consultation records)",
20
+ description: "List an organization's labels (consultation records) with filtering, sorting, and offset pagination. " +
21
+ "Common types: ClinicConversation (live recording), NxHubBatch (NxHub button recording), NxMIC (NxHub automatic conversation), DictationAudio, AggregateLabel. " +
22
+ "NOTE: transcripts and SOAP notes (ownedPatientNotes) are ALWAYS null in this list — use get_label for those. " +
23
+ "The `metadata` field is a JSON string that must be parsed. fromTime/toTime are Unix epoch milliseconds. " +
24
+ "The response is prefixed with the total record count (from the X-Total-Count header).",
25
+ inputSchema: {
26
+ organizationId: z.string().uuid().describe("Organization UUID (discover via get_identity)"),
27
+ types: z
28
+ .array(z.enum(LABEL_TYPES))
29
+ .min(1)
30
+ .describe("Record types to retrieve"),
31
+ limit: z.number().int().min(1).max(100).default(20).describe("Max records to return"),
32
+ offset: z.number().int().min(0).default(0).describe("Records to skip (pagination)"),
33
+ sortingProperty: z.enum(["FromTime", "Type", "Patient"]).default("FromTime"),
34
+ isSortingDescending: z.boolean().default(true),
35
+ createdBy: z.string().uuid().optional().describe("Filter by creating user ID"),
36
+ patientSearch: z.string().optional().describe("Filter by patient name substring"),
37
+ },
38
+ }, async (args) => run(async () => {
39
+ const { organizationId, ...query } = args;
40
+ const res = await client.get(`/api/organizations/${organizationId}/labels`, query);
41
+ const total = res.headers.get("X-Total-Count");
42
+ return jsonResult(res.body, total !== null ? `Total matching records: ${total}` : undefined);
43
+ }));
44
+ server.registerTool("get_label", {
45
+ title: "Get label details (transcript + SOAP)",
46
+ description: "Get full details for one label, including ownedPatientNotes with Transcript and SOAP note content. " +
47
+ "SOAP note `content` and label `metadata` are JSON strings that must be parsed. " +
48
+ 'A SOAP content of "{}" means AI processing has not completed yet.',
49
+ inputSchema: {
50
+ labelId: z.string().uuid().describe("Label (record) ID"),
51
+ },
52
+ }, async ({ labelId }) => run(async () => jsonResult((await client.get(`/api/labels/${labelId}`)).body)));
53
+ server.registerTool("assign_patient_to_label", {
54
+ title: "Assign patient to label",
55
+ description: "Assign a patient to an existing label (e.g. move a recording off Default Patient).",
56
+ inputSchema: {
57
+ labelId: z.string().uuid().describe("Label (record) ID"),
58
+ patientId: z.string().uuid().describe("Patient ID to assign"),
59
+ },
60
+ }, async ({ labelId, patientId }) => run(async () => {
61
+ await client.post(`/api/labels/${labelId}/assign`, { body: { patientId } });
62
+ return jsonResult({ assigned: true, labelId, patientId });
63
+ }));
64
+ server.registerTool("aggregate_labels", {
65
+ title: "Aggregate labels",
66
+ description: "Combine multiple labels into a single aggregated record, optionally applying a medical template " +
67
+ "(see list_medical_templates). Returns the new aggregated record.",
68
+ inputSchema: {
69
+ labelIds: z.array(z.string().uuid()).min(1).describe("Source label IDs to combine"),
70
+ patientId: z.string().uuid().describe("Patient the aggregated record belongs to"),
71
+ medicalTemplateId: z.string().uuid().optional().describe("Medical template to apply"),
72
+ },
73
+ }, async ({ labelIds, patientId, medicalTemplateId }) => run(async () => {
74
+ const res = await client.post("/api/labels/aggregate", {
75
+ body: {
76
+ LabelIds: labelIds,
77
+ PatientId: patientId,
78
+ ...(medicalTemplateId ? { MedicalTemplateId: medicalTemplateId } : {}),
79
+ },
80
+ });
81
+ return jsonResult(res.body);
82
+ }));
83
+ server.registerTool("regenerate_label", {
84
+ title: "Regenerate label output",
85
+ description: "Trigger async reprocessing/regeneration of a label's AI output (e.g. after changing the medical template). " +
86
+ "Fire-and-forget: a 504 gateway timeout from the API still means the job was triggered successfully. " +
87
+ "Poll get_label afterwards to see the refreshed notes.",
88
+ inputSchema: {
89
+ labelId: z.string().uuid().describe("Label (record) ID to regenerate"),
90
+ medicalTemplateIds: z
91
+ .array(z.string().uuid())
92
+ .optional()
93
+ .describe("Medical template IDs to apply"),
94
+ },
95
+ }, async ({ labelId, medicalTemplateIds }) => run(async () => {
96
+ const res = await client.postLambdaRegenerate(labelId, {
97
+ medicalTemplateIds,
98
+ });
99
+ return jsonResult({
100
+ triggered: true,
101
+ labelId,
102
+ httpStatus: res.status,
103
+ note: res.status === 504
104
+ ? "504 is expected for this endpoint — regeneration was triggered and runs asynchronously."
105
+ : "Regeneration triggered.",
106
+ });
107
+ }));
108
+ server.registerTool("list_medical_templates", {
109
+ title: "List medical templates",
110
+ description: "List an organization's medical templates (used with aggregate_labels and regenerate_label). " +
111
+ "Template `content` is a JSON string that must be parsed.",
112
+ inputSchema: {
113
+ organizationId: z.string().uuid().describe("Organization UUID"),
114
+ },
115
+ }, async ({ organizationId }) => run(async () => jsonResult((await client.get(`/api/organizations/${organizationId}/medical-templates`)).body)));
116
+ }
117
+ //# sourceMappingURL=labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labels.js","sourceRoot":"","sources":["../../src/tools/labels.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { jsonResult, run } from "../format.js";
3
+ export function registerWebhookTools(server, client) {
4
+ server.registerTool("list_webhooks", {
5
+ title: "List webhooks",
6
+ description: "List an organization's registered webhooks. Secrets are never returned.",
7
+ inputSchema: {
8
+ organizationId: z.string().uuid().describe("Organization UUID"),
9
+ },
10
+ }, async ({ organizationId }) => run(async () => jsonResult((await client.get(`/api/organizations/${organizationId}/webhooks`)).body)));
11
+ server.registerTool("create_webhook", {
12
+ title: "Create webhook",
13
+ description: "Register a webhook endpoint (HTTPS only, max 10 per organization). " +
14
+ "IMPORTANT: the HMAC signing `secret` in the response is shown ONLY ONCE — the user must store it immediately.",
15
+ inputSchema: {
16
+ organizationId: z.string().uuid().describe("Organization UUID"),
17
+ url: z.string().url().describe("HTTPS endpoint to deliver events to"),
18
+ description: z.string().optional().describe("Human-readable description"),
19
+ eventTypes: z
20
+ .array(z.string())
21
+ .min(1)
22
+ .describe('Event types to subscribe to, e.g. ["conversation_created", "conversation_completed"]'),
23
+ },
24
+ }, async ({ organizationId, ...body }) => run(async () => jsonResult((await client.post(`/api/organizations/${organizationId}/webhooks`, { body })).body, "Save the `secret` now — it is shown only once and is required for HMAC signature verification.")));
25
+ server.registerTool("delete_webhook", {
26
+ title: "Delete webhook",
27
+ description: "Delete a webhook and all its delivery logs. Irreversible.",
28
+ inputSchema: {
29
+ organizationId: z.string().uuid().describe("Organization UUID"),
30
+ webhookId: z.string().uuid().describe("Webhook ID"),
31
+ },
32
+ }, async ({ organizationId, webhookId }) => run(async () => {
33
+ await client.delete(`/api/organizations/${organizationId}/webhooks/${webhookId}`);
34
+ return jsonResult({ deleted: true, webhookId });
35
+ }));
36
+ server.registerTool("test_webhook", {
37
+ title: "Send webhook test ping",
38
+ description: "Send a test `ping` event to a webhook endpoint to verify connectivity.",
39
+ inputSchema: {
40
+ organizationId: z.string().uuid().describe("Organization UUID"),
41
+ webhookId: z.string().uuid().describe("Webhook ID"),
42
+ },
43
+ }, async ({ organizationId, webhookId }) => run(async () => {
44
+ const res = await client.post(`/api/organizations/${organizationId}/webhooks/${webhookId}/test`);
45
+ return jsonResult(res.body ?? { pingSent: true, webhookId });
46
+ }));
47
+ server.registerTool("enable_webhook", {
48
+ title: "Re-enable webhook",
49
+ description: "Re-enable a webhook that was auto-disabled after consecutive delivery failures. Resets the failure counter.",
50
+ inputSchema: {
51
+ organizationId: z.string().uuid().describe("Organization UUID"),
52
+ webhookId: z.string().uuid().describe("Webhook ID"),
53
+ },
54
+ }, async ({ organizationId, webhookId }) => run(async () => {
55
+ const res = await client.post(`/api/organizations/${organizationId}/webhooks/${webhookId}/enable`);
56
+ return jsonResult(res.body ?? { enabled: true, webhookId });
57
+ }));
58
+ server.registerTool("get_webhook_deliveries", {
59
+ title: "View webhook delivery logs",
60
+ description: "View the last 50 delivery attempts for a webhook: status codes, response times, and error messages. " +
61
+ "Useful for debugging why a webhook endpoint is not receiving events.",
62
+ inputSchema: {
63
+ organizationId: z.string().uuid().describe("Organization UUID"),
64
+ webhookId: z.string().uuid().describe("Webhook ID"),
65
+ },
66
+ }, async ({ organizationId, webhookId }) => run(async () => jsonResult((await client.get(`/api/organizations/${organizationId}/webhooks/${webhookId}/deliveries`))
67
+ .body)));
68
+ }
69
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/tools/webhooks.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "nxvet-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the NxVET API (labels, transcripts, NxHub conversations, webhooks, API keys) — hosted at https://mcp.nx.vet/mcp or run locally via npx",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "nxvet-mcp": "dist/stdio.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/nervextech/nxvet-mcp.git"
17
+ },
18
+ "homepage": "https://api.nx.vet/mcp.html",
19
+ "license": "MIT",
20
+ "keywords": [
21
+ "mcp",
22
+ "modelcontextprotocol",
23
+ "nxvet",
24
+ "veterinary"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "start": "node dist/index.js",
32
+ "dev": "tsx watch src/index.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.20.0",
38
+ "express": "^4.21.2",
39
+ "zod": "^3.25.76"
40
+ },
41
+ "devDependencies": {
42
+ "@types/express": "^4.17.21",
43
+ "@types/node": "^22.15.0",
44
+ "tsx": "^4.19.0",
45
+ "typescript": "^5.7.3"
46
+ }
47
+ }