btrainr-mcp 1.0.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 +15 -0
- package/README.md +159 -0
- package/dist/api/leadClient.js +82 -0
- package/dist/api/userClient.js +47 -0
- package/dist/config.js +34 -0
- package/dist/server.js +87 -0
- package/dist/services/baseService.js +69 -0
- package/dist/services/clientService.js +40 -0
- package/dist/services/interactionService.js +31 -0
- package/dist/services/leadService.js +40 -0
- package/dist/tools/btrainrTools.js +186 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Btrainr
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Btrainr MCP Server
|
|
2
|
+
|
|
3
|
+
An implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) for the **Btrainr** coaching platform. This server exposes Btrainr functionalities as tools that can be used by LLMs (Claude, Cursor, OpenAI, etc.) to manage clients, leads, check-ins, and communications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **MCP Standard Compliant**: Implements the Model Context Protocol over standard I/O (Stdio) by default, with optional SSE support.
|
|
8
|
+
- **Type-Safe Tools**: All tools are defined with Zod schemas for robust validation.
|
|
9
|
+
- **Built-in Safety Gates**: Write operations (`create_lead`, `update_lead`, `send_message`, `add_client_tag`, `create_client_note`) require explicit confirmation (`confirm: true`) and support dry runs (`dry_run: true`).
|
|
10
|
+
- **Client & Lead Management**: Query and update clients, check-ins, notes, and leads directly from your AI assistant.
|
|
11
|
+
- **Interactive Chat CLI**: Built-in CLI chat client to test tools interactively using OpenAI models.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick Start (with `npx`)
|
|
16
|
+
|
|
17
|
+
You can run the Btrainr MCP server directly without cloning or manually building:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx -y btrainr-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Claude Desktop Configuration
|
|
24
|
+
|
|
25
|
+
Add the following to your `claude_desktop_config.json` (located at `%APPDATA%\Claude\claude_desktop_config.json` on Windows, or `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"btrainr": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "btrainr-mcp"],
|
|
33
|
+
"env": {
|
|
34
|
+
"BTRAINR_API_KEY": "your_btrainr_api_key_here",
|
|
35
|
+
"BTRAINR_API_URL": "https://beta.btrainr.com/api"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Cursor Configuration
|
|
43
|
+
|
|
44
|
+
Add to your `.cursor/mcp.json` or Cursor MCP settings:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"btrainr": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "btrainr-mcp"],
|
|
52
|
+
"env": {
|
|
53
|
+
"BTRAINR_API_KEY": "your_btrainr_api_key_here",
|
|
54
|
+
"BTRAINR_API_URL": "https://beta.btrainr.com/api"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Architecture
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Local Installation & Development
|
|
70
|
+
|
|
71
|
+
### 1. Clone & Install Dependencies
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://gitlab.com/Bcoder24/btrainr-mcp.git
|
|
74
|
+
cd btrainr-mcp
|
|
75
|
+
npm install
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2. Configure Environment
|
|
79
|
+
Copy `.env.example` to `.env` and fill in your credentials:
|
|
80
|
+
```bash
|
|
81
|
+
cp .env.example .env
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```env
|
|
85
|
+
BTRAINR_API_KEY=your_btrainr_api_key_here
|
|
86
|
+
BTRAINR_API_URL=https://beta.btrainr.com/api
|
|
87
|
+
PORT=3000
|
|
88
|
+
OPENAI_API_KEY=sk-your-openai-api-key-here # Only needed for scripts/chat.ts
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Build
|
|
92
|
+
```bash
|
|
93
|
+
npm run build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 4. Run MCP Server
|
|
97
|
+
- **Stdio Mode (Default)**:
|
|
98
|
+
```bash
|
|
99
|
+
npm start
|
|
100
|
+
```
|
|
101
|
+
- **SSE Mode (HTTP)**:
|
|
102
|
+
```bash
|
|
103
|
+
node dist/server.js --sse --port 3000
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 5. Interactive Chat CLI
|
|
107
|
+
Test your MCP tools using an interactive conversational agent powered by OpenAI:
|
|
108
|
+
```bash
|
|
109
|
+
npm run chat
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Available Tools
|
|
115
|
+
|
|
116
|
+
### Client Tools
|
|
117
|
+
- **`list_clients`**: Lists clients in the workspace. Supports filtering by `status` (`active`, `inactive`, `pending`) and pagination (`limit`, `skip`).
|
|
118
|
+
- **`get_client_profile`**: Retrieves detailed profile information for a client by ID.
|
|
119
|
+
- **`get_client_checkins`**: Fetches recent check-in history for a client.
|
|
120
|
+
- **`add_client_tag`**: Adds a tag (e.g. `'vip'`, `'injury'`) to a client. _Requires `confirm: true`._
|
|
121
|
+
- **`send_message`**: Sends a message to a client. _Requires `confirm: true`._
|
|
122
|
+
- **`create_client_note`**: Creates a private note for a client. _Requires `confirm: true`._
|
|
123
|
+
|
|
124
|
+
### Lead Tools
|
|
125
|
+
- **`list_leads`**: Lists leads in the current workspace with pagination and status filters.
|
|
126
|
+
- **`get_lead`**: Gets detailed profile for a specific lead by ID.
|
|
127
|
+
- **`create_lead`**: Creates a new lead with contact information (`name`, `email`, `phone`). _Requires `confirm: true`._
|
|
128
|
+
- **`update_lead`**: Updates lead details (`name`, `email`, `phone`). _Requires `confirm: true`._
|
|
129
|
+
|
|
130
|
+
### Safety Gates on Write Actions
|
|
131
|
+
Every modifying action inherits the safety schema:
|
|
132
|
+
- `confirm` *(boolean)*: Must be `true` to execute the mutation. If omitted or `false`, the operation is blocked.
|
|
133
|
+
- `dry_run` *(boolean, optional)*: Simulates the execution without committing changes.
|
|
134
|
+
- `reason` *(string, optional)*: Audit rationale recorded in service logs.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Publishing to npm
|
|
139
|
+
|
|
140
|
+
To publish this package to npm:
|
|
141
|
+
|
|
142
|
+
1. Log in to your npm account:
|
|
143
|
+
```bash
|
|
144
|
+
npm login
|
|
145
|
+
```
|
|
146
|
+
2. Verify packaging contents:
|
|
147
|
+
```bash
|
|
148
|
+
npm pack --dry-run
|
|
149
|
+
```
|
|
150
|
+
3. Publish:
|
|
151
|
+
```bash
|
|
152
|
+
npm publish --access public
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
[ISC](LICENSE) © Btrainr
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.leadClient = exports.LeadClient = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
class LeadClient {
|
|
10
|
+
client;
|
|
11
|
+
constructor() {
|
|
12
|
+
this.client = axios_1.default.create({
|
|
13
|
+
baseURL: config_1.config.BTRAINR_API_URL,
|
|
14
|
+
headers: {
|
|
15
|
+
'Authorization': `Bearer ${config_1.config.BTRAINR_API_KEY}`,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async createLead(payload) {
|
|
21
|
+
try {
|
|
22
|
+
const response = await this.client.post('/generateLeads', payload); // Assuming endpoint is /leads
|
|
23
|
+
return response.data;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
27
|
+
throw new Error(`Failed to create lead: ${error.message} - ${JSON.stringify(error.response?.data)}`);
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async getListLeads(params) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await this.client.get('/generateLeads', { params });
|
|
35
|
+
return response.data;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
39
|
+
throw new Error(`Failed to fetch leads: ${error.message} - ${JSON.stringify(error.response?.data)}`);
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async getLead(id) {
|
|
45
|
+
try {
|
|
46
|
+
const response = await this.client.get(`/generateLeads/${id}`);
|
|
47
|
+
return response.data;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
51
|
+
throw new Error(`Failed to fetch lead: ${error.message} - ${JSON.stringify(error.response?.data)}`);
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async updateLead(id, payload) {
|
|
57
|
+
try {
|
|
58
|
+
const response = await this.client.put(`/generateLeads/${id}`, payload);
|
|
59
|
+
return response.data;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
63
|
+
throw new Error(`Failed to update lead: ${error.message} - ${JSON.stringify(error.response?.data)}`);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async deleteLead(id) {
|
|
69
|
+
try {
|
|
70
|
+
const response = await this.client.delete(`/generateLeads/${id}`);
|
|
71
|
+
return response.data;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
75
|
+
throw new Error(`Failed to delete lead: ${error.message} - ${JSON.stringify(error.response?.data)}`);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.LeadClient = LeadClient;
|
|
82
|
+
exports.leadClient = new LeadClient();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.userClient = exports.UserClient = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
class UserClient {
|
|
10
|
+
client;
|
|
11
|
+
constructor() {
|
|
12
|
+
this.client = axios_1.default.create({
|
|
13
|
+
baseURL: config_1.config.BTRAINR_API_URL,
|
|
14
|
+
headers: {
|
|
15
|
+
'Authorization': `Bearer ${config_1.config.BTRAINR_API_KEY}`,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async getUsers(params) {
|
|
21
|
+
try {
|
|
22
|
+
const response = await this.client.get('/users', { params });
|
|
23
|
+
return response.data;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
27
|
+
throw new Error(`Failed to fetch users: ${error.message} - ${JSON.stringify(error.response?.data)}`);
|
|
28
|
+
}
|
|
29
|
+
console.error("UserClient error:", error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async getUser(id) {
|
|
34
|
+
try {
|
|
35
|
+
const response = await this.client.get(`/users/${id}`);
|
|
36
|
+
return response.data;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
40
|
+
throw new Error(`Failed to fetch user: ${error.message} - ${JSON.stringify(error.response?.data)}`);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.UserClient = UserClient;
|
|
47
|
+
exports.userClient = new UserClient();
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.config = exports.getSystemContext = void 0;
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
dotenv_1.default.config();
|
|
10
|
+
const configSchema = zod_1.z.object({
|
|
11
|
+
BTRAINR_API_KEY: zod_1.z.string({
|
|
12
|
+
required_error: "BTRAINR_API_KEY is required. Please set it in your environment or .env file.",
|
|
13
|
+
}).min(1, "BTRAINR_API_KEY cannot be empty"),
|
|
14
|
+
BTRAINR_API_URL: zod_1.z.string().url("BTRAINR_API_URL must be a valid URL").default("https://beta.btrainr.com/api"),
|
|
15
|
+
PORT: zod_1.z.string().default("3000"),
|
|
16
|
+
MCP_TRANSPORT: zod_1.z.enum(['stdio', 'sse']).default('stdio'),
|
|
17
|
+
});
|
|
18
|
+
const getSystemContext = () => ({
|
|
19
|
+
requestId: crypto.randomUUID()
|
|
20
|
+
});
|
|
21
|
+
exports.getSystemContext = getSystemContext;
|
|
22
|
+
const parsedConfig = configSchema.safeParse(process.env);
|
|
23
|
+
if (!parsedConfig.success) {
|
|
24
|
+
console.error("❌ Btrainr MCP Server configuration error:");
|
|
25
|
+
const errors = parsedConfig.error.format();
|
|
26
|
+
if (errors.BTRAINR_API_KEY?._errors?.length) {
|
|
27
|
+
console.error(" - Missing BTRAINR_API_KEY. Configure it in your MCP client settings (e.g. claude_desktop_config.json) or .env file.");
|
|
28
|
+
}
|
|
29
|
+
if (errors.BTRAINR_API_URL?._errors?.length) {
|
|
30
|
+
console.error(" - Invalid BTRAINR_API_URL. It must be a valid URL (e.g., https://btrainr.com/api).");
|
|
31
|
+
}
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
exports.config = parsedConfig.data;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.createServer = createServer;
|
|
8
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
10
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
11
|
+
const express_1 = __importDefault(require("express"));
|
|
12
|
+
const btrainrTools_1 = require("./tools/btrainrTools");
|
|
13
|
+
// Factory to create a new server instance with all tools registered
|
|
14
|
+
function createServer() {
|
|
15
|
+
const server = new mcp_js_1.McpServer({
|
|
16
|
+
name: "btrainr-mcp",
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
});
|
|
19
|
+
for (const tool of btrainrTools_1.allTools) {
|
|
20
|
+
server.registerTool(tool.name, {
|
|
21
|
+
description: tool.description,
|
|
22
|
+
inputSchema: tool.inputSchema,
|
|
23
|
+
}, tool.handler);
|
|
24
|
+
}
|
|
25
|
+
return server;
|
|
26
|
+
}
|
|
27
|
+
// Transport & Server execution
|
|
28
|
+
async function startSseServer(port) {
|
|
29
|
+
const app = (0, express_1.default)();
|
|
30
|
+
const transports = new Map();
|
|
31
|
+
let globalTransport;
|
|
32
|
+
app.get("/sse", async (req, res) => {
|
|
33
|
+
console.error("New SSE connection established");
|
|
34
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport();
|
|
35
|
+
const server = createServer();
|
|
36
|
+
await server.connect(transport);
|
|
37
|
+
await transport.handleRequest(req, res);
|
|
38
|
+
globalTransport = transport;
|
|
39
|
+
if (req.query.sessionId) {
|
|
40
|
+
transports.set(req.query.sessionId, transport);
|
|
41
|
+
console.error(`Mapped session ${req.query.sessionId} to transport`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
app.post("/message", async (req, res) => {
|
|
45
|
+
console.error("Received HTTP message");
|
|
46
|
+
const sessionId = req.query.sessionId || req.headers['mcp-session-id'];
|
|
47
|
+
let transportToUse = globalTransport;
|
|
48
|
+
if (sessionId && transports.has(sessionId)) {
|
|
49
|
+
transportToUse = transports.get(sessionId);
|
|
50
|
+
console.error(`Using mapped transport for session ${sessionId}`);
|
|
51
|
+
}
|
|
52
|
+
if (transportToUse) {
|
|
53
|
+
await transportToUse.handleRequest(req, res);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
res.status(400).send("No active transport");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
app.listen(port, () => {
|
|
60
|
+
console.error(`Btrainr MCP Server (SSE mode) running on http://localhost:${port}/sse`);
|
|
61
|
+
console.error(`Registered ${btrainrTools_1.allTools.length} tools.`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function startStdioServer() {
|
|
65
|
+
const server = createServer();
|
|
66
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
67
|
+
await server.connect(transport);
|
|
68
|
+
console.error("Btrainr MCP Server running on stdio");
|
|
69
|
+
console.error(`Registered ${btrainrTools_1.allTools.length} tools.`);
|
|
70
|
+
}
|
|
71
|
+
async function main() {
|
|
72
|
+
const args = process.argv.slice(2);
|
|
73
|
+
const isSse = args.includes('--sse') || args.includes('--transport=sse') || process.env.MCP_TRANSPORT === 'sse';
|
|
74
|
+
const portArgIndex = args.findIndex(arg => arg === '--port' || arg === '-p');
|
|
75
|
+
const portFromArg = portArgIndex !== -1 ? parseInt(args[portArgIndex + 1], 10) : undefined;
|
|
76
|
+
const port = portFromArg || (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000);
|
|
77
|
+
if (isSse) {
|
|
78
|
+
await startSseServer(port);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
await startStdioServer();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
main().catch((error) => {
|
|
85
|
+
console.error("Server error:", error);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseService = void 0;
|
|
4
|
+
class BaseService {
|
|
5
|
+
serviceName;
|
|
6
|
+
constructor(name) {
|
|
7
|
+
this.serviceName = name;
|
|
8
|
+
}
|
|
9
|
+
log(ctx, action, details) {
|
|
10
|
+
console.error(JSON.stringify({
|
|
11
|
+
timestamp: new Date().toISOString(),
|
|
12
|
+
level: 'INFO',
|
|
13
|
+
service: this.serviceName,
|
|
14
|
+
requestId: ctx.requestId,
|
|
15
|
+
action,
|
|
16
|
+
details: this.redact(details)
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
warn(ctx, action, message) {
|
|
20
|
+
console.error(JSON.stringify({
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
level: 'WARN',
|
|
23
|
+
service: this.serviceName,
|
|
24
|
+
requestId: ctx.requestId,
|
|
25
|
+
action,
|
|
26
|
+
message
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
redact(obj) {
|
|
30
|
+
// Simple redaction for now
|
|
31
|
+
const redacted = { ...obj };
|
|
32
|
+
if (redacted.password)
|
|
33
|
+
redacted.password = '[REDACTED]';
|
|
34
|
+
if (redacted.token)
|
|
35
|
+
redacted.token = '[REDACTED]';
|
|
36
|
+
return redacted;
|
|
37
|
+
}
|
|
38
|
+
async performWriteAction(ctx, actionName, options, executor) {
|
|
39
|
+
if (options.dryRun) {
|
|
40
|
+
this.log(ctx, `${actionName}_DRY_RUN`, { options });
|
|
41
|
+
return {
|
|
42
|
+
dryRun: true,
|
|
43
|
+
message: `[Review Mode] Action '${actionName}' would be executed. Reason: ${options.reason || 'No reason provided'}. params: ${JSON.stringify(this.redact(options))}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (!options.confirm) {
|
|
47
|
+
this.warn(ctx, `${actionName}_BLOCKED`, "Missing confirmation");
|
|
48
|
+
throw new Error(`Safety Gate: Action '${actionName}' requires confirm=true to execute. Please review arguments and try again.`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
this.log(ctx, `${actionName}_START`, { options });
|
|
52
|
+
const result = await executor();
|
|
53
|
+
this.log(ctx, `${actionName}_SUCCESS`, { resultId: result?.id });
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error(JSON.stringify({
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
level: 'ERROR',
|
|
60
|
+
service: this.serviceName,
|
|
61
|
+
requestId: ctx.requestId,
|
|
62
|
+
action: actionName,
|
|
63
|
+
error: error.message
|
|
64
|
+
}));
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.BaseService = BaseService;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clientService = exports.ClientService = void 0;
|
|
4
|
+
const baseService_1 = require("./baseService");
|
|
5
|
+
const userClient_1 = require("../api/userClient");
|
|
6
|
+
class ClientService extends baseService_1.BaseService {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('ClientService');
|
|
9
|
+
}
|
|
10
|
+
async listClients(ctx, params) {
|
|
11
|
+
// Enforce tenancy by passing ID to the API
|
|
12
|
+
// In a real app, the API token might imply this, or we pass headers
|
|
13
|
+
// For V1 simulation, we might filter client-side if API doesn't support specific scoping yet
|
|
14
|
+
this.log(ctx, 'list_clients', { params });
|
|
15
|
+
// Real implementation:
|
|
16
|
+
const pageSize = params.limit || 10;
|
|
17
|
+
const pageNo = params.skip || 1;
|
|
18
|
+
return await userClient_1.userClient.getUsers({
|
|
19
|
+
pageSize,
|
|
20
|
+
pageNo,
|
|
21
|
+
status: params.status,
|
|
22
|
+
role: params.role
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async getClientProfile(ctx, clientId) {
|
|
26
|
+
this.log(ctx, 'get_client_profile', { clientId });
|
|
27
|
+
// Ensure the client belongs to the workspace (Resource Check)
|
|
28
|
+
// if (client.workspaceId !== ctx.workspaceId) throw new Error("Unauthorized access to client");
|
|
29
|
+
return await userClient_1.userClient.getUser(clientId);
|
|
30
|
+
}
|
|
31
|
+
async addClientTag(ctx, clientId, tag, options) {
|
|
32
|
+
return this.performWriteAction(ctx, 'add_client_tag', options, async () => {
|
|
33
|
+
// Mock API Call
|
|
34
|
+
// await apiClient.post(`/clients/${clientId}/tags`, { tag });
|
|
35
|
+
return { success: true, clientId, addedTag: tag };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.ClientService = ClientService;
|
|
40
|
+
exports.clientService = new ClientService();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.interactionService = exports.InteractionService = void 0;
|
|
4
|
+
const baseService_1 = require("./baseService");
|
|
5
|
+
class InteractionService extends baseService_1.BaseService {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('InteractionService');
|
|
8
|
+
}
|
|
9
|
+
async sendMessage(ctx, clientId, content, options) {
|
|
10
|
+
return this.performWriteAction(ctx, 'send_message', options, async () => {
|
|
11
|
+
// Mock API: await apiClient.post(`/clients/${clientId}/messages`, { content });
|
|
12
|
+
return { sent: true, to: clientId, contentShort: content.substring(0, 20) + '...' };
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async getCheckIns(ctx, clientId, params) {
|
|
16
|
+
this.log(ctx, 'get_check_ins', { clientId, params });
|
|
17
|
+
// Mock API
|
|
18
|
+
return [
|
|
19
|
+
{ id: 'chk1', date: '2023-10-27', weight: 75.5, compliance: 'high' },
|
|
20
|
+
{ id: 'chk2', date: '2023-10-20', weight: 76.0, compliance: 'medium' }
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
async createNote(ctx, clientId, note, options) {
|
|
24
|
+
return this.performWriteAction(ctx, 'create_note', options, async () => {
|
|
25
|
+
// Mock API
|
|
26
|
+
return { id: 'note_' + Date.now(), clientId, note_preview: note.substring(0, 50) };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.InteractionService = InteractionService;
|
|
31
|
+
exports.interactionService = new InteractionService();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.leadService = exports.LeadService = void 0;
|
|
4
|
+
const leadClient_1 = require("../api/leadClient");
|
|
5
|
+
const baseService_1 = require("./baseService");
|
|
6
|
+
class LeadService extends baseService_1.BaseService {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('LeadService');
|
|
9
|
+
}
|
|
10
|
+
async createNewLead(ctx, data, options = {}) {
|
|
11
|
+
return this.performWriteAction(ctx, 'create_lead', options, async () => {
|
|
12
|
+
return await leadClient_1.leadClient.createLead(data);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async getListLeads(ctx, params) {
|
|
16
|
+
const queryParams = {
|
|
17
|
+
...params,
|
|
18
|
+
pageSize: params?.limit ?? 10,
|
|
19
|
+
pageNo: params?.skip ?? 1,
|
|
20
|
+
};
|
|
21
|
+
this.log(ctx, 'list_leads', { params: queryParams });
|
|
22
|
+
return await leadClient_1.leadClient.getListLeads(queryParams);
|
|
23
|
+
}
|
|
24
|
+
async getLeadById(ctx, id) {
|
|
25
|
+
this.log(ctx, 'get_lead', { id });
|
|
26
|
+
return await leadClient_1.leadClient.getLead(id);
|
|
27
|
+
}
|
|
28
|
+
async updateLead(ctx, id, data, options = {}) {
|
|
29
|
+
return this.performWriteAction(ctx, 'update_lead', options, async () => {
|
|
30
|
+
return await leadClient_1.leadClient.updateLead(id, data);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async deleteLead(ctx, id, options = {}) {
|
|
34
|
+
return this.performWriteAction(ctx, 'delete_lead', options, async () => {
|
|
35
|
+
return await leadClient_1.leadClient.deleteLead(id);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.LeadService = LeadService;
|
|
40
|
+
exports.leadService = new LeadService();
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.allTools = exports.updateLeadTool = exports.createLeadTool = exports.getLeadTool = exports.listLeadsTool = exports.createNoteTool = exports.addClientTagTool = exports.sendMessageTool = exports.getClientCheckinsTool = exports.getClientProfileTool = exports.listClientsTool = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const clientService_1 = require("../services/clientService");
|
|
6
|
+
const interactionService_1 = require("../services/interactionService");
|
|
7
|
+
const leadService_1 = require("../services/leadService");
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
// Reusable Schemas
|
|
10
|
+
const ActionSchema = zod_1.z.object({
|
|
11
|
+
confirm: zod_1.z.boolean().optional().describe("Set to true to execute the action. If false/missing, action fails (Safety Gate)."),
|
|
12
|
+
dry_run: zod_1.z.boolean().optional().describe("If true, simulates the action without making changes."),
|
|
13
|
+
reason: zod_1.z.string().optional().describe("Reason for this action (for audit logs).")
|
|
14
|
+
});
|
|
15
|
+
// --- READ TOOLS ---
|
|
16
|
+
exports.listClientsTool = {
|
|
17
|
+
name: "list_clients",
|
|
18
|
+
description: "List clients in the current workspace. Returns basic info (ID, name, status).",
|
|
19
|
+
inputSchema: zod_1.z.object({
|
|
20
|
+
limit: zod_1.z.number().optional().default(10),
|
|
21
|
+
skip: zod_1.z.number().optional().default(1),
|
|
22
|
+
status: zod_1.z.enum(['active', 'inactive', "pending"]).optional()
|
|
23
|
+
}),
|
|
24
|
+
handler: async (args) => {
|
|
25
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
26
|
+
const clients = await clientService_1.clientService.listClients(ctx, args);
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: JSON.stringify(clients, null, 2) }]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
exports.getClientProfileTool = {
|
|
33
|
+
name: "get_client_profile",
|
|
34
|
+
description: "Get detailed profile for a specific client.",
|
|
35
|
+
inputSchema: zod_1.z.object({
|
|
36
|
+
client_id: zod_1.z.string()
|
|
37
|
+
}),
|
|
38
|
+
handler: async (args) => {
|
|
39
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
40
|
+
const profile = await clientService_1.clientService.getClientProfile(ctx, args.client_id);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: JSON.stringify(profile, null, 2) }]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
exports.getClientCheckinsTool = {
|
|
47
|
+
name: "get_client_checkins",
|
|
48
|
+
description: "Get recent check-in history for a client.",
|
|
49
|
+
inputSchema: zod_1.z.object({
|
|
50
|
+
client_id: zod_1.z.string(),
|
|
51
|
+
limit: zod_1.z.number().optional().default(5)
|
|
52
|
+
}),
|
|
53
|
+
handler: async (args) => {
|
|
54
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
55
|
+
const checkins = await interactionService_1.interactionService.getCheckIns(ctx, args.client_id, args);
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify(checkins, null, 2) }]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
// --- WRITE TOOLS ---
|
|
62
|
+
exports.sendMessageTool = {
|
|
63
|
+
name: "send_message",
|
|
64
|
+
description: "Send a message to a client. Requires confirmation.",
|
|
65
|
+
inputSchema: ActionSchema.extend({
|
|
66
|
+
client_id: zod_1.z.string(),
|
|
67
|
+
message: zod_1.z.string().min(1),
|
|
68
|
+
}),
|
|
69
|
+
handler: async (args) => {
|
|
70
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
71
|
+
const { client_id, message, confirm, dry_run, reason } = args;
|
|
72
|
+
const result = await interactionService_1.interactionService.sendMessage(ctx, client_id, message, { confirm, dryRun: dry_run, reason });
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
exports.addClientTagTool = {
|
|
79
|
+
name: "add_client_tag",
|
|
80
|
+
description: "Add a tag to a client (e.g. 'vip', 'injury'). Requires confirmation.",
|
|
81
|
+
inputSchema: ActionSchema.extend({
|
|
82
|
+
client_id: zod_1.z.string(),
|
|
83
|
+
tag: zod_1.z.string().min(1),
|
|
84
|
+
}),
|
|
85
|
+
handler: async (args) => {
|
|
86
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
87
|
+
const { client_id, tag, confirm, dry_run, reason } = args;
|
|
88
|
+
const result = await clientService_1.clientService.addClientTag(ctx, client_id, tag, { confirm, dryRun: dry_run, reason });
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
exports.createNoteTool = {
|
|
95
|
+
name: "create_client_note",
|
|
96
|
+
description: "Create a private note for a client. Requires confirmation.",
|
|
97
|
+
inputSchema: ActionSchema.extend({
|
|
98
|
+
client_id: zod_1.z.string(),
|
|
99
|
+
note: zod_1.z.string().min(1),
|
|
100
|
+
}),
|
|
101
|
+
handler: async (args) => {
|
|
102
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
103
|
+
const { client_id, note, confirm, dry_run, reason } = args;
|
|
104
|
+
const result = await interactionService_1.interactionService.createNote(ctx, client_id, note, { confirm, dryRun: dry_run, reason });
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
exports.listLeadsTool = {
|
|
111
|
+
name: "list_leads",
|
|
112
|
+
description: "List leads in the current workspace. Returns basic info (ID, name, status).",
|
|
113
|
+
inputSchema: zod_1.z.object({
|
|
114
|
+
limit: zod_1.z.number().optional().default(10),
|
|
115
|
+
skip: zod_1.z.number().optional().default(1),
|
|
116
|
+
status: zod_1.z.enum(['active', 'inactive', "pending"]).optional()
|
|
117
|
+
}),
|
|
118
|
+
handler: async (args) => {
|
|
119
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
120
|
+
const leads = await leadService_1.leadService.getListLeads(ctx, args);
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: JSON.stringify(leads, null, 2) }]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
exports.getLeadTool = {
|
|
127
|
+
name: "get_lead",
|
|
128
|
+
description: "Get detailed profile for a specific lead.",
|
|
129
|
+
inputSchema: zod_1.z.object({
|
|
130
|
+
lead_id: zod_1.z.string()
|
|
131
|
+
}),
|
|
132
|
+
handler: async (args) => {
|
|
133
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
134
|
+
const lead = await leadService_1.leadService.getLeadById(ctx, args.lead_id);
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: JSON.stringify(lead, null, 2) }]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
exports.createLeadTool = {
|
|
141
|
+
name: "create_lead",
|
|
142
|
+
description: "Create a new lead. Requires confirmation.",
|
|
143
|
+
inputSchema: ActionSchema.extend({
|
|
144
|
+
name: zod_1.z.string().min(1),
|
|
145
|
+
email: zod_1.z.string().email().optional(),
|
|
146
|
+
phone: zod_1.z.string().optional(),
|
|
147
|
+
}),
|
|
148
|
+
handler: async (args) => {
|
|
149
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
150
|
+
const { name, email, phone, confirm, dry_run, reason } = args;
|
|
151
|
+
const result = await leadService_1.leadService.createNewLead(ctx, { name, email, phone }, { confirm, dryRun: dry_run, reason });
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
exports.updateLeadTool = {
|
|
158
|
+
name: "update_lead",
|
|
159
|
+
description: "Update a lead. Requires confirmation.",
|
|
160
|
+
inputSchema: ActionSchema.extend({
|
|
161
|
+
lead_id: zod_1.z.string(),
|
|
162
|
+
name: zod_1.z.string().min(1).optional(),
|
|
163
|
+
email: zod_1.z.string().email().optional(),
|
|
164
|
+
phone: zod_1.z.string().optional(),
|
|
165
|
+
}),
|
|
166
|
+
handler: async (args) => {
|
|
167
|
+
const ctx = (0, config_1.getSystemContext)();
|
|
168
|
+
const { lead_id, name, email, phone, confirm, dry_run, reason } = args;
|
|
169
|
+
const result = await leadService_1.leadService.updateLead(ctx, lead_id, { name, email, phone }, { confirm, dryRun: dry_run, reason });
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
exports.allTools = [
|
|
176
|
+
exports.listClientsTool,
|
|
177
|
+
exports.getClientProfileTool,
|
|
178
|
+
exports.getClientCheckinsTool,
|
|
179
|
+
exports.sendMessageTool,
|
|
180
|
+
exports.addClientTagTool,
|
|
181
|
+
exports.createNoteTool,
|
|
182
|
+
exports.listLeadsTool,
|
|
183
|
+
exports.getLeadTool,
|
|
184
|
+
exports.createLeadTool,
|
|
185
|
+
exports.updateLeadTool
|
|
186
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "btrainr-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Model Context Protocol (MCP) server for the Btrainr coaching platform",
|
|
5
|
+
"main": "dist/server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"btrainr-mcp": "./dist/server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
|
|
16
|
+
"prebuild": "npm run clean",
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "node dist/server.js",
|
|
19
|
+
"dev": "ts-node src/server.ts",
|
|
20
|
+
"chat": "ts-node scripts/chat.ts",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"btrainr",
|
|
27
|
+
"claude",
|
|
28
|
+
"llm",
|
|
29
|
+
"ai-tools"
|
|
30
|
+
],
|
|
31
|
+
"author": "Btrainr",
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://gitlab.com/Bcoder24/btrainr-mcp.git"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.0.1",
|
|
39
|
+
"axios": "^1.6.0",
|
|
40
|
+
"dotenv": "^16.4.0",
|
|
41
|
+
"express": "^5.2.1",
|
|
42
|
+
"zod": "^3.22.0",
|
|
43
|
+
"zod-to-json-schema": "^3.25.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/express": "^5.0.6",
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"openai": "^6.22.0",
|
|
49
|
+
"ts-node": "^10.9.0",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|