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 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
+ ![Architecture](./public/architecture.png)
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
+ }