brioright-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/.env ADDED
@@ -0,0 +1,3 @@
1
+ BRIORIGHT_API_URL=http://localhost:3001/api
2
+ BRIORIGHT_API_KEY=brio_9e4e5415fd92d4a3768918a938149f5533c16ae7ac2157ba786cf33dd815a63d
3
+ BRIORIGHT_WORKSPACE_ID=welcomet
package/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ # ── Brioright API connection ──────────────────────────────────────────────────
2
+ BRIORIGHT_API_URL=https://brioright.online/api
3
+ BRIORIGHT_API_KEY=brio_your_key_here
4
+ BRIORIGHT_WORKSPACE_ID=your-workspace-slug
5
+
6
+ # ── Transport mode ────────────────────────────────────────────────────────────
7
+ # Leave unset (or set to "stdio") for Claude Desktop / Cursor
8
+ # Set to "http" for cloud AI clients (Antigravity, remote access)
9
+ MCP_TRANSPORT=http
10
+
11
+ # ── HTTP/SSE mode settings (only used when MCP_TRANSPORT=http) ────────────────
12
+ MCP_PORT=4040
13
+ # Optional bearer token — cloud AI clients must pass: Authorization: Bearer <secret>
14
+ MCP_SECRET=change_me_to_a_strong_secret
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Brioright MCP Server
2
+
3
+ Connect AI assistants (Claude Desktop, Cursor, Antigravity) directly to your Brioright workspace.
4
+
5
+ ## Transport Modes
6
+ This server supports two transport modes:
7
+ 1. **Stdio mode (Local)**: Runs as a subprocess for local clients like Claude Desktop or Cursor.
8
+ 2. **HTTP/SSE mode (Cloud)**: Runs as a persistent web server for cloud-based AI assistants (like Antigravity).
9
+
10
+ ## Quick Setup
11
+
12
+ ### 1. Generate an API Key
13
+ Log in to Brioright, then run from your browser console:
14
+ ```javascript
15
+ const res = await fetch('/api/api-keys', {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ credentials: 'include',
19
+ body: JSON.stringify({ name: 'My MCP Client' })
20
+ });
21
+ console.log(await res.json());
22
+ ```
23
+ **Save the key**!
24
+
25
+ ### 2. Configure the MCP Server
26
+
27
+ ```bash
28
+ cp .env.example .env
29
+ ```
30
+ Edit `.env`:
31
+ ```env
32
+ BRIORIGHT_API_URL=https://brioright.online/api
33
+ BRIORIGHT_API_KEY=brio_your_key_here
34
+ BRIORIGHT_WORKSPACE_ID=your-workspace-slug
35
+
36
+ # Leave unset (or "stdio") for Claude Desktop
37
+ # Set to "http" for cloud clients like Antigravity
38
+ MCP_TRANSPORT=http
39
+ MCP_PORT=4040
40
+ MCP_SECRET=change_me_to_a_strong_secret
41
+ ```
42
+
43
+ ### 3A. Connect to Claude Desktop (Local)
44
+ Add to `%APPDATA%\Claude\claude_desktop_config.json`:
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "brioright": {
49
+ "command": "node",
50
+ "args": ["/absolute/path/to/mcp-server/index.js"],
51
+ "env": {
52
+ "BRIORIGHT_API_URL": "...",
53
+ "BRIORIGHT_API_KEY": "...",
54
+ "BRIORIGHT_WORKSPACE_ID": "..."
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### 3B. Connect Cloud AI (Remote)
62
+ Deploy the server with `MCP_TRANSPORT=http` (via PM2).
63
+ Provide the cloud AI assistant your MCP server base URL: `http://your-server-ip:4040/sse`
64
+ And pass the Authorization Header: `Bearer your_secure_bearer_token`.
65
+
66
+ ## Available Tools
67
+ | Tool | Description |
68
+ |------|-------------|
69
+ | `list_workspaces` | List all accessible workspaces |
70
+ | `list_projects` | List projects in a workspace |
71
+ | `list_tasks` | List tasks with optional status/priority filter |
72
+ | `get_task` | Get full task details |
73
+ | `create_task` | Create a task with title, priority, due date, assignee |
74
+ | `update_task` | Update any fields on a task |
75
+ | `complete_task` | Mark a task as done |
76
+ | `create_project` | Create a new project |
77
+ | `list_members` | List workspace members (for finding assignee IDs) |
78
+ | `get_workspace_summary` | Dashboard stats: task counts by status/priority |
package/index.js ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Brioright MCP Server
4
+ *
5
+ * Supports two transport modes:
6
+ * 1. stdio (default) — for local clients: Claude Desktop, Cursor
7
+ * 2. http (MCP_TRANSPORT=http) — for cloud AI: Antigravity, remote clients
8
+ *
9
+ * Usage:
10
+ * node index.js # stdio mode
11
+ * MCP_TRANSPORT=http node index.js # HTTP/SSE mode (port from MCP_PORT, default 4040)
12
+ */
13
+
14
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
16
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
17
+ import { z } from 'zod'
18
+ import axios from 'axios'
19
+ import express from 'express'
20
+ import cors from 'cors'
21
+ import { config } from 'dotenv'
22
+ import { fileURLToPath } from 'url'
23
+ import { dirname, join } from 'path'
24
+
25
+ // Load .env from mcp-server directory
26
+ const __dirname = dirname(fileURLToPath(import.meta.url))
27
+ config({ path: join(__dirname, '.env') })
28
+
29
+ const API_URL = process.env.BRIORIGHT_API_URL || 'http://localhost:3001/api'
30
+ const API_KEY = process.env.BRIORIGHT_API_KEY
31
+ const DEFAULT_WORKSPACE = process.env.BRIORIGHT_WORKSPACE_ID
32
+ const MCP_PORT = parseInt(process.env.MCP_PORT || '4040')
33
+ const MCP_SECRET = process.env.MCP_SECRET // Optional bearer token for HTTP mode
34
+ const USE_HTTP = process.env.MCP_TRANSPORT === 'http'
35
+
36
+ if (!API_KEY) {
37
+ process.stderr.write('[Brioright MCP] ERROR: BRIORIGHT_API_KEY is not set.\n')
38
+ process.exit(1)
39
+ }
40
+
41
+ // ── Axios client ──────────────────────────────────────────────────────────────
42
+ const api = axios.create({
43
+ baseURL: API_URL,
44
+ headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
45
+ timeout: 10000,
46
+ })
47
+
48
+ async function call(method, path, data) {
49
+ try {
50
+ const res = await api({ method, url: path, data })
51
+ return res.data.data
52
+ } catch (err) {
53
+ const msg = err.response?.data?.message || err.message || 'Unknown error'
54
+ throw new Error(`Brioright API error: ${msg}`)
55
+ }
56
+ }
57
+
58
+ // ── Build MCP Server (shared between both transports) ────────────────────────
59
+ function buildServer() {
60
+ const server = new McpServer({ name: 'brioright', version: '1.0.0' })
61
+
62
+ // ── list_workspaces ───────────────────────────────────────────────────────
63
+ server.tool('list_workspaces', 'List all Brioright workspaces the API key has access to', {},
64
+ async () => {
65
+ const data = await call('GET', '/workspaces')
66
+ const workspaces = data.workspaces || data
67
+ return { content: [{ type: 'text', text: JSON.stringify(workspaces.map(w => ({ id: w.id, slug: w.slug, name: w.name })), null, 2) }] }
68
+ }
69
+ )
70
+
71
+ // ── list_projects ─────────────────────────────────────────────────────────
72
+ server.tool('list_projects', 'List all projects in a workspace',
73
+ { workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.') },
74
+ async ({ workspaceId }) => {
75
+ const ws = workspaceId || DEFAULT_WORKSPACE
76
+ if (!ws) throw new Error('workspaceId is required')
77
+ const data = await call('GET', `/workspaces/${ws}/projects`)
78
+ const projects = data.projects || data
79
+ return { content: [{ type: 'text', text: JSON.stringify(projects.map(p => ({ id: p.id, name: p.name, status: p.status })), null, 2) }] }
80
+ }
81
+ )
82
+
83
+ // ── list_tasks ────────────────────────────────────────────────────────────
84
+ server.tool('list_tasks', 'List tasks in a project with optional filters',
85
+ {
86
+ projectId: z.string().describe('Project ID'),
87
+ workspaceId: z.string().optional(),
88
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional(),
89
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
90
+ limit: z.number().optional().default(20),
91
+ },
92
+ async ({ projectId, workspaceId, status, priority, limit }) => {
93
+ const ws = workspaceId || DEFAULT_WORKSPACE
94
+ if (!ws) throw new Error('workspaceId is required')
95
+ const params = new URLSearchParams()
96
+ if (status) params.set('status', status)
97
+ if (priority) params.set('priority', priority)
98
+ params.set('limit', String(limit || 20))
99
+ const data = await call('GET', `/workspaces/${ws}/projects/${projectId}/tasks?${params}`)
100
+ const tasks = data.tasks || data
101
+ return { content: [{ type: 'text', text: JSON.stringify(tasks.map(t => ({ id: t.id, title: t.title, status: t.status, priority: t.priority, dueDate: t.dueDate, assignee: t.assignee?.name })), null, 2) }] }
102
+ }
103
+ )
104
+
105
+ // ── get_task ──────────────────────────────────────────────────────────────
106
+ server.tool('get_task', 'Get full details of a single task',
107
+ { taskId: z.string(), workspaceId: z.string().optional() },
108
+ async ({ taskId, workspaceId }) => {
109
+ const ws = workspaceId || DEFAULT_WORKSPACE
110
+ if (!ws) throw new Error('workspaceId is required')
111
+ const data = await call('GET', `/workspaces/${ws}/tasks/${taskId}`)
112
+ return { content: [{ type: 'text', text: JSON.stringify(data.task || data, null, 2) }] }
113
+ }
114
+ )
115
+
116
+ // ── create_task ───────────────────────────────────────────────────────────
117
+ server.tool('create_task', 'Create a new task in a Brioright project',
118
+ {
119
+ projectId: z.string().describe('Project ID'),
120
+ title: z.string().describe('Task title'),
121
+ description: z.string().optional(),
122
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done']).optional().default('todo'),
123
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium'),
124
+ dueDate: z.string().optional().describe('ISO date string e.g. 2026-03-15'),
125
+ assigneeId: z.string().optional(),
126
+ workspaceId: z.string().optional(),
127
+ },
128
+ async ({ projectId, title, description, status, priority, dueDate, assigneeId, workspaceId }) => {
129
+ const ws = workspaceId || DEFAULT_WORKSPACE
130
+ if (!ws) throw new Error('workspaceId is required')
131
+ const data = await call('POST', `/workspaces/${ws}/projects/${projectId}/tasks`, {
132
+ title, description, status, priority,
133
+ dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
134
+ assigneeId,
135
+ })
136
+ const task = data.task || data
137
+ return { content: [{ type: 'text', text: `✅ Task created!\n\n${JSON.stringify({ id: task.id, title: task.title, status: task.status, priority: task.priority, dueDate: task.dueDate }, null, 2)}` }] }
138
+ }
139
+ )
140
+
141
+ // ── update_task ───────────────────────────────────────────────────────────
142
+ server.tool('update_task', 'Update fields on an existing task',
143
+ {
144
+ taskId: z.string(),
145
+ title: z.string().optional(),
146
+ description: z.string().optional(),
147
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional(),
148
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
149
+ dueDate: z.string().optional(),
150
+ assigneeId: z.string().optional(),
151
+ workspaceId: z.string().optional(),
152
+ },
153
+ async ({ taskId, workspaceId, ...fields }) => {
154
+ const ws = workspaceId || DEFAULT_WORKSPACE
155
+ if (!ws) throw new Error('workspaceId is required')
156
+ const updates = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined))
157
+ if (updates.dueDate) updates.dueDate = new Date(updates.dueDate).toISOString()
158
+ const data = await call('PATCH', `/workspaces/${ws}/tasks/${taskId}`, updates)
159
+ const task = data.task || data
160
+ return { content: [{ type: 'text', text: `✅ Task updated!\n\n${JSON.stringify({ id: task.id, title: task.title, status: task.status, priority: task.priority }, null, 2)}` }] }
161
+ }
162
+ )
163
+
164
+ // ── complete_task ─────────────────────────────────────────────────────────
165
+ server.tool('complete_task', 'Mark a task as completed',
166
+ { taskId: z.string(), workspaceId: z.string().optional() },
167
+ async ({ taskId, workspaceId }) => {
168
+ const ws = workspaceId || DEFAULT_WORKSPACE
169
+ if (!ws) throw new Error('workspaceId is required')
170
+ await call('PATCH', `/workspaces/${ws}/tasks/${taskId}`, { status: 'done' })
171
+ return { content: [{ type: 'text', text: `✅ Task ${taskId} marked as done.` }] }
172
+ }
173
+ )
174
+
175
+ // ── create_project ────────────────────────────────────────────────────────
176
+ server.tool('create_project', 'Create a new project in a workspace',
177
+ {
178
+ name: z.string(),
179
+ description: z.string().optional(),
180
+ color: z.string().optional().default('#6366f1'),
181
+ workspaceId: z.string().optional(),
182
+ },
183
+ async ({ name, description, color, workspaceId }) => {
184
+ const ws = workspaceId || DEFAULT_WORKSPACE
185
+ if (!ws) throw new Error('workspaceId is required')
186
+ const data = await call('POST', `/workspaces/${ws}/projects`, { name, description, color })
187
+ const project = data.project || data
188
+ return { content: [{ type: 'text', text: `✅ Project created!\n\n${JSON.stringify({ id: project.id, name: project.name }, null, 2)}` }] }
189
+ }
190
+ )
191
+
192
+ // ── list_members ──────────────────────────────────────────────────────────
193
+ server.tool('list_members', 'List workspace members (useful for finding assignee IDs)',
194
+ { workspaceId: z.string().optional() },
195
+ async ({ workspaceId }) => {
196
+ const ws = workspaceId || DEFAULT_WORKSPACE
197
+ if (!ws) throw new Error('workspaceId is required')
198
+ const data = await call('GET', `/workspaces/${ws}/members`)
199
+ const members = data.members || data
200
+ return { content: [{ type: 'text', text: JSON.stringify(members.map(m => ({ id: m.user?.id || m.id, name: m.user?.name || m.name, email: m.user?.email || m.email, role: m.role })), null, 2) }] }
201
+ }
202
+ )
203
+
204
+ // ── get_workspace_summary ─────────────────────────────────────────────────
205
+ server.tool('get_workspace_summary', 'Dashboard stats: task counts by status and priority',
206
+ { workspaceId: z.string().optional() },
207
+ async ({ workspaceId }) => {
208
+ const ws = workspaceId || DEFAULT_WORKSPACE
209
+ if (!ws) throw new Error('workspaceId is required')
210
+ const data = await call('GET', `/workspaces/${ws}/dashboard/stats`)
211
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
212
+ }
213
+ )
214
+
215
+ return server
216
+ }
217
+
218
+ // ── HTTP/SSE transport (for cloud AI clients) ─────────────────────────────────
219
+ if (USE_HTTP) {
220
+ const app = express()
221
+ app.use(express.json())
222
+ app.use(cors({
223
+ origin: '*', // Cloud AI clients can come from any origin
224
+ exposedHeaders: ['Content-Type', 'Cache-Control', 'X-Accel-Buffering'],
225
+ }))
226
+
227
+ // Optional bearer token auth for the HTTP endpoint
228
+ app.use((req, res, next) => {
229
+ if (MCP_SECRET) {
230
+ const authHeader = req.headers.authorization || ''
231
+ const token = authHeader.replace('Bearer ', '')
232
+ if (token !== MCP_SECRET) {
233
+ return res.status(401).json({ error: 'Unauthorized — invalid MCP_SECRET' })
234
+ }
235
+ }
236
+ next()
237
+ })
238
+
239
+ // Track active SSE transports by session
240
+ const transports = new Map()
241
+
242
+ // SSE endpoint — client connects here first to get a session
243
+ app.get('/sse', async (req, res) => {
244
+ process.stderr.write(`[Brioright MCP] New SSE connection from ${req.ip}\n`)
245
+ const transport = new SSEServerTransport('/messages', res)
246
+ transports.set(transport.sessionId, transport)
247
+
248
+ // Clean up on disconnect
249
+ res.on('close', () => {
250
+ process.stderr.write(`[Brioright MCP] SSE session ${transport.sessionId} closed\n`)
251
+ transports.delete(transport.sessionId)
252
+ })
253
+
254
+ const server = buildServer()
255
+ await server.connect(transport)
256
+ })
257
+
258
+ // Messages endpoint — tool call requests come here
259
+ app.post('/messages', async (req, res) => {
260
+ const sessionId = req.query.sessionId
261
+ const transport = transports.get(sessionId)
262
+ if (!transport) {
263
+ return res.status(404).json({ error: `Session ${sessionId} not found` })
264
+ }
265
+ await transport.handlePostMessage(req, res)
266
+ })
267
+
268
+ // Health check
269
+ app.get('/health', (req, res) => res.json({ status: 'ok', server: 'brioright-mcp', transport: 'http/sse' }))
270
+
271
+ app.listen(MCP_PORT, () => {
272
+ process.stderr.write(`[Brioright MCP] HTTP/SSE server running on port ${MCP_PORT}\n`)
273
+ process.stderr.write(`[Brioright MCP] SSE endpoint: http://0.0.0.0:${MCP_PORT}/sse\n`)
274
+ process.stderr.write(`[Brioright MCP] Messages endpoint: http://0.0.0.0:${MCP_PORT}/messages\n`)
275
+ })
276
+
277
+ // ── Stdio transport (for local clients: Claude Desktop, Cursor) ───────────────
278
+ } else {
279
+ const server = buildServer()
280
+ const transport = new StdioServerTransport()
281
+ await server.connect(transport)
282
+ process.stderr.write('[Brioright MCP] Stdio server running. Listening for MCP requests...\n')
283
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "brioright-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Brioright — lets AI assistants create and manage tasks via natural language",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "brioright-mcp": "index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "dev": "node --watch index.js",
13
+ "inspect": "npx @modelcontextprotocol/inspector node index.js"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.7.0",
17
+ "axios": "^1.7.0",
18
+ "cors": "^2.8.6",
19
+ "dotenv": "^16.4.0",
20
+ "express": "^4.22.1"
21
+ },
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ }
25
+ }