@flowrelay/mcp-server 0.3.8 → 0.4.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/README.md +101 -0
- package/dist/api.d.ts +46 -4
- package/dist/api.js +25 -6
- package/dist/index.js +173 -17
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Flow Relay MCP Server
|
|
2
|
+
|
|
3
|
+
Flow Relay MCP Server adds project-aware, multi-tenant Flow Relay tools to MCP clients such as Claude Desktop and Claude Code.
|
|
4
|
+
|
|
5
|
+
It connects to Flow Relay API v1 using an API key and supports both:
|
|
6
|
+
|
|
7
|
+
- Personal stream scope
|
|
8
|
+
- Project scope (personal project or organization project)
|
|
9
|
+
|
|
10
|
+
## Package
|
|
11
|
+
|
|
12
|
+
- Name: @flowrelay/mcp-server
|
|
13
|
+
- Version: 0.4.0
|
|
14
|
+
|
|
15
|
+
## What Is Included
|
|
16
|
+
|
|
17
|
+
The server currently exposes these tools:
|
|
18
|
+
|
|
19
|
+
- get_workspace_context
|
|
20
|
+
- list_projects
|
|
21
|
+
- set_active_project
|
|
22
|
+
- list_handoffs
|
|
23
|
+
- generate_handoff
|
|
24
|
+
- list_integrations
|
|
25
|
+
- list_events
|
|
26
|
+
- discord_list_channels
|
|
27
|
+
- discord_send_message
|
|
28
|
+
|
|
29
|
+
## Environment Variables
|
|
30
|
+
|
|
31
|
+
Required:
|
|
32
|
+
|
|
33
|
+
- FLOWRELAY_API_KEY
|
|
34
|
+
|
|
35
|
+
Optional:
|
|
36
|
+
|
|
37
|
+
- FLOWRELAY_PROJECT_ID
|
|
38
|
+
- FLOWRELAY_BASE_URL
|
|
39
|
+
|
|
40
|
+
If FLOWRELAY_PROJECT_ID is set, it becomes the default project context for project-aware tools unless you override it per call.
|
|
41
|
+
|
|
42
|
+
## Quick Start (Claude Desktop)
|
|
43
|
+
|
|
44
|
+
Add this to your claude_desktop_config.json:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"flowrelay": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "@flowrelay/mcp-server"],
|
|
52
|
+
"env": {
|
|
53
|
+
"FLOWRELAY_API_KEY": "fr_your_api_key_here",
|
|
54
|
+
"FLOWRELAY_PROJECT_ID": "optional_project_id"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Multi-Tenant Behavior
|
|
62
|
+
|
|
63
|
+
- If no project is active, tools run in personal stream scope.
|
|
64
|
+
- You can select a project during the MCP session with set_active_project.
|
|
65
|
+
- You can override scope per call by passing project_id where supported.
|
|
66
|
+
|
|
67
|
+
Recommended flow:
|
|
68
|
+
|
|
69
|
+
1. Call get_workspace_context
|
|
70
|
+
2. Call list_projects
|
|
71
|
+
3. Call set_active_project
|
|
72
|
+
4. Run handoff and query tools in the selected scope
|
|
73
|
+
|
|
74
|
+
## Local Development
|
|
75
|
+
|
|
76
|
+
From this folder:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install
|
|
80
|
+
npm run build
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Create a tarball package:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm pack
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Troubleshooting
|
|
90
|
+
|
|
91
|
+
- Error: Missing FLOWRELAY_API_KEY
|
|
92
|
+
- Set FLOWRELAY_API_KEY in your MCP client configuration.
|
|
93
|
+
- Project not found or inaccessible
|
|
94
|
+
- Run list_projects and use one of the returned IDs.
|
|
95
|
+
- No events or handoffs returned
|
|
96
|
+
- Verify active scope and data availability in that scope.
|
|
97
|
+
|
|
98
|
+
## Related Docs
|
|
99
|
+
|
|
100
|
+
- Repository overview: ../README.md
|
|
101
|
+
- Release notes: ../RELEASE_NOTES_v0.4.0.md
|
package/dist/api.d.ts
CHANGED
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Flow Relay API client — talks to flowrelay.it/api/v1/*
|
|
3
3
|
*/
|
|
4
|
+
export type AccountType = 'personal' | 'business';
|
|
5
|
+
export type AccessRole = 'owner' | 'admin' | 'member';
|
|
6
|
+
export interface TenantOrganization {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
role: 'admin' | 'member';
|
|
11
|
+
is_temporary_admin: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface TenantProject {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
description: string;
|
|
18
|
+
organization_id: string | null;
|
|
19
|
+
organization_name: string | null;
|
|
20
|
+
organization_slug: string | null;
|
|
21
|
+
project_type: 'personal' | 'organization';
|
|
22
|
+
access_role: AccessRole;
|
|
23
|
+
created_at: string;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
}
|
|
26
|
+
export interface TenantContext {
|
|
27
|
+
account_type: AccountType;
|
|
28
|
+
organizations: TenantOrganization[];
|
|
29
|
+
projects: TenantProject[];
|
|
30
|
+
}
|
|
4
31
|
export declare class FlowRelayAPI {
|
|
5
32
|
private baseUrl;
|
|
6
33
|
private apiKey;
|
|
7
34
|
constructor(apiKey: string, baseUrl?: string);
|
|
8
35
|
private request;
|
|
9
|
-
|
|
36
|
+
listProjects(): Promise<TenantContext>;
|
|
37
|
+
listHandoffs(status?: string, limit?: number, projectId?: string | null): Promise<{
|
|
10
38
|
handoffs: Array<{
|
|
11
39
|
id: string;
|
|
40
|
+
user_id: string;
|
|
41
|
+
project_id: string | null;
|
|
42
|
+
project_name?: string | null;
|
|
43
|
+
scope_type?: "personal" | "project";
|
|
12
44
|
title: string;
|
|
13
45
|
summary: string;
|
|
14
46
|
status: string;
|
|
@@ -24,9 +56,13 @@ export declare class FlowRelayAPI {
|
|
|
24
56
|
generateHandoff(sources?: string[], filters?: Record<string, {
|
|
25
57
|
projects?: string[];
|
|
26
58
|
eventTypes?: string[];
|
|
27
|
-
}
|
|
59
|
+
}>, projectId?: string | null): Promise<{
|
|
28
60
|
handoff: {
|
|
29
61
|
id: string;
|
|
62
|
+
user_id: string;
|
|
63
|
+
project_id: string | null;
|
|
64
|
+
project_name?: string | null;
|
|
65
|
+
scope_type?: "personal" | "project";
|
|
30
66
|
title: string;
|
|
31
67
|
summary: string;
|
|
32
68
|
sources: string[];
|
|
@@ -37,17 +73,23 @@ export declare class FlowRelayAPI {
|
|
|
37
73
|
created_at: string;
|
|
38
74
|
};
|
|
39
75
|
}>;
|
|
40
|
-
listIntegrations(): Promise<{
|
|
76
|
+
listIntegrations(projectId?: string | null): Promise<{
|
|
41
77
|
integrations: Array<{
|
|
42
78
|
source: string;
|
|
43
79
|
workspace_id: string | null;
|
|
44
80
|
workspace_name: string | null;
|
|
45
81
|
connected_at: string;
|
|
82
|
+
scope?: "personal" | "project";
|
|
83
|
+
resource_type?: string | null;
|
|
84
|
+
connection_status?: string | null;
|
|
85
|
+
providers_connected?: number;
|
|
86
|
+
last_validated_at?: string | null;
|
|
46
87
|
}>;
|
|
47
88
|
}>;
|
|
48
|
-
listEvents(source?: string, limit?: number): Promise<{
|
|
89
|
+
listEvents(source?: string, limit?: number, projectId?: string | null): Promise<{
|
|
49
90
|
events: Array<{
|
|
50
91
|
id: string;
|
|
92
|
+
user_id?: string;
|
|
51
93
|
source: string;
|
|
52
94
|
event_type: string;
|
|
53
95
|
title: string;
|
package/dist/api.js
CHANGED
|
@@ -23,30 +23,49 @@ export class FlowRelayAPI {
|
|
|
23
23
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
24
24
|
throw new Error(body.error ?? `API error ${res.status}`);
|
|
25
25
|
}
|
|
26
|
+
if (res.status === 204) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
26
29
|
return res.json();
|
|
27
30
|
}
|
|
28
|
-
async
|
|
29
|
-
return this.request(
|
|
31
|
+
async listProjects() {
|
|
32
|
+
return this.request('/projects');
|
|
30
33
|
}
|
|
31
|
-
async
|
|
34
|
+
async listHandoffs(status = 'active', limit = 10, projectId) {
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
params.set('status', status);
|
|
37
|
+
params.set('limit', String(limit));
|
|
38
|
+
if (projectId)
|
|
39
|
+
params.set('project_id', projectId);
|
|
40
|
+
return this.request(`/handoffs?${params.toString()}`);
|
|
41
|
+
}
|
|
42
|
+
async generateHandoff(sources, filters, projectId) {
|
|
32
43
|
const body = {};
|
|
33
44
|
if (sources?.length)
|
|
34
45
|
body.sources = sources;
|
|
35
46
|
if (filters && Object.keys(filters).length > 0)
|
|
36
47
|
body.filters = filters;
|
|
48
|
+
if (projectId)
|
|
49
|
+
body.project_id = projectId;
|
|
37
50
|
return this.request('/handoffs', {
|
|
38
51
|
method: 'POST',
|
|
39
52
|
body: JSON.stringify(body),
|
|
40
53
|
});
|
|
41
54
|
}
|
|
42
|
-
async listIntegrations() {
|
|
43
|
-
|
|
55
|
+
async listIntegrations(projectId) {
|
|
56
|
+
const params = new URLSearchParams();
|
|
57
|
+
if (projectId)
|
|
58
|
+
params.set('project_id', projectId);
|
|
59
|
+
const suffix = params.toString();
|
|
60
|
+
return this.request(`/integrations${suffix ? `?${suffix}` : ''}`);
|
|
44
61
|
}
|
|
45
|
-
async listEvents(source, limit = 20) {
|
|
62
|
+
async listEvents(source, limit = 20, projectId) {
|
|
46
63
|
const params = new URLSearchParams();
|
|
47
64
|
if (source)
|
|
48
65
|
params.set('source', source);
|
|
49
66
|
params.set('limit', String(limit));
|
|
67
|
+
if (projectId)
|
|
68
|
+
params.set('project_id', projectId);
|
|
50
69
|
return this.request(`/events?${params}`);
|
|
51
70
|
}
|
|
52
71
|
async discordListChannels() {
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,43 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { FlowRelayAPI } from './api.js';
|
|
6
|
+
const SOURCES = [
|
|
7
|
+
'github',
|
|
8
|
+
'slack',
|
|
9
|
+
'discord',
|
|
10
|
+
'linear',
|
|
11
|
+
'notion',
|
|
12
|
+
'confluence',
|
|
13
|
+
'jira',
|
|
14
|
+
'gitlab',
|
|
15
|
+
'bitbucket',
|
|
16
|
+
'azure_devops',
|
|
17
|
+
'figma',
|
|
18
|
+
'microsoft_outlook',
|
|
19
|
+
'microsoft_teams',
|
|
20
|
+
'sentry',
|
|
21
|
+
'datadog',
|
|
22
|
+
];
|
|
23
|
+
const SourceEnum = z.enum(SOURCES);
|
|
24
|
+
function normalizeProjectId(value) {
|
|
25
|
+
if (typeof value !== 'string')
|
|
26
|
+
return null;
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
29
|
+
}
|
|
30
|
+
function formatProjectScope(project) {
|
|
31
|
+
if (!project)
|
|
32
|
+
return 'No project (personal stream)';
|
|
33
|
+
if (project.project_type === 'personal') {
|
|
34
|
+
return `Personal project: ${project.name}`;
|
|
35
|
+
}
|
|
36
|
+
const role = project.access_role === 'admin'
|
|
37
|
+
? 'admin'
|
|
38
|
+
: project.access_role === 'member'
|
|
39
|
+
? 'member'
|
|
40
|
+
: 'owner';
|
|
41
|
+
return `Org project: ${project.name} (${project.organization_name ?? 'Unknown org'}, ${role})`;
|
|
42
|
+
}
|
|
6
43
|
const apiKey = process.env.FLOWRELAY_API_KEY;
|
|
7
44
|
if (!apiKey) {
|
|
8
45
|
console.error('Error: FLOWRELAY_API_KEY environment variable is required.');
|
|
@@ -10,22 +47,121 @@ if (!apiKey) {
|
|
|
10
47
|
process.exit(1);
|
|
11
48
|
}
|
|
12
49
|
const api = new FlowRelayAPI(apiKey, process.env.FLOWRELAY_BASE_URL);
|
|
50
|
+
let activeProjectId = normalizeProjectId(process.env.FLOWRELAY_PROJECT_ID);
|
|
13
51
|
const server = new McpServer({
|
|
14
52
|
name: 'flowrelay',
|
|
15
|
-
version: '0.
|
|
53
|
+
version: '0.4.0',
|
|
54
|
+
});
|
|
55
|
+
async function getTenantContext() {
|
|
56
|
+
const context = await api.listProjects();
|
|
57
|
+
if (activeProjectId && !context.projects.some((project) => project.id === activeProjectId)) {
|
|
58
|
+
activeProjectId = null;
|
|
59
|
+
}
|
|
60
|
+
return context;
|
|
61
|
+
}
|
|
62
|
+
async function resolveProject(projectId) {
|
|
63
|
+
const explicitProjectId = normalizeProjectId(projectId);
|
|
64
|
+
const resolvedProjectId = explicitProjectId ?? activeProjectId;
|
|
65
|
+
if (!resolvedProjectId) {
|
|
66
|
+
return { projectId: null, project: null };
|
|
67
|
+
}
|
|
68
|
+
const context = await getTenantContext();
|
|
69
|
+
const project = context.projects.find((candidate) => candidate.id === resolvedProjectId) ?? null;
|
|
70
|
+
if (!project) {
|
|
71
|
+
activeProjectId = null;
|
|
72
|
+
return { projectId: null, project: null };
|
|
73
|
+
}
|
|
74
|
+
return { projectId: resolvedProjectId, project };
|
|
75
|
+
}
|
|
76
|
+
// ── Tool: workspace context ─────────────────────────────────────────
|
|
77
|
+
server.tool('get_workspace_context', 'Show Flow Relay tenant context: personal/business mode, active project scope, and current role.', {}, async () => {
|
|
78
|
+
const context = await getTenantContext();
|
|
79
|
+
const activeProject = activeProjectId
|
|
80
|
+
? context.projects.find((project) => project.id === activeProjectId) ?? null
|
|
81
|
+
: null;
|
|
82
|
+
const isBusiness = context.account_type === 'business' || context.organizations.length > 0;
|
|
83
|
+
const roleLine = activeProject
|
|
84
|
+
? `Role: ${activeProject.access_role}`
|
|
85
|
+
: 'Role: n/a (no project selected)';
|
|
86
|
+
const lines = [
|
|
87
|
+
`Account mode: ${isBusiness ? 'business' : 'personal'}`,
|
|
88
|
+
`Organizations: ${context.organizations.length}`,
|
|
89
|
+
`Accessible projects: ${context.projects.length}`,
|
|
90
|
+
`Active scope: ${formatProjectScope(activeProject)}`,
|
|
91
|
+
roleLine,
|
|
92
|
+
];
|
|
93
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
94
|
+
});
|
|
95
|
+
// ── Tool: list projects ─────────────────────────────────────────────
|
|
96
|
+
server.tool('list_projects', 'List all projects available to this API key, including personal and organization projects with role information.', {}, async () => {
|
|
97
|
+
const context = await getTenantContext();
|
|
98
|
+
if (context.projects.length === 0) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: 'No accessible projects found. Use personal stream or get added to a project team.',
|
|
103
|
+
}],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const lines = context.projects.map((project) => {
|
|
107
|
+
const active = project.id === activeProjectId ? ' [active]' : '';
|
|
108
|
+
const scope = project.project_type === 'personal'
|
|
109
|
+
? 'personal'
|
|
110
|
+
: `org:${project.organization_name ?? 'unknown'}`;
|
|
111
|
+
return `- ${project.name} (${scope}, role=${project.access_role})\n id: ${project.id}${active}`;
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: `Projects:\n${lines.join('\n')}`,
|
|
117
|
+
}],
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
// ── Tool: set active project ────────────────────────────────────────
|
|
121
|
+
server.tool('set_active_project', 'Set or clear the active project context used by project-aware tools when project_id is omitted.', {
|
|
122
|
+
project_id: z.string().optional().describe('Project ID to set as active. Omit to clear.'),
|
|
123
|
+
clear: z.boolean().default(false).describe('Clear active project and use no-project personal scope.'),
|
|
124
|
+
}, async ({ project_id, clear }) => {
|
|
125
|
+
if (clear || !normalizeProjectId(project_id)) {
|
|
126
|
+
activeProjectId = null;
|
|
127
|
+
return { content: [{ type: 'text', text: 'Active project cleared. Using no-project personal scope.' }] };
|
|
128
|
+
}
|
|
129
|
+
const context = await getTenantContext();
|
|
130
|
+
const selected = context.projects.find((project) => project.id === project_id);
|
|
131
|
+
if (!selected) {
|
|
132
|
+
return {
|
|
133
|
+
content: [{
|
|
134
|
+
type: 'text',
|
|
135
|
+
text: `Project not found or inaccessible: ${project_id}. Run list_projects first.`,
|
|
136
|
+
}],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
activeProjectId = selected.id;
|
|
140
|
+
return {
|
|
141
|
+
content: [{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: `Active project set to ${selected.name} (${selected.id}).`,
|
|
144
|
+
}],
|
|
145
|
+
};
|
|
16
146
|
});
|
|
17
147
|
// ── Tool: list handoffs ──────────────────────────────────────────────
|
|
18
|
-
server.tool('list_handoffs', 'List
|
|
148
|
+
server.tool('list_handoffs', 'List Flow Relay handoffs in the current tenant scope (personal stream or selected project).', {
|
|
19
149
|
status: z.enum(['active', 'archived', 'completed']).default('active').describe('Filter by status'),
|
|
20
150
|
limit: z.number().min(1).max(50).default(10).describe('Max number of handoffs to return'),
|
|
21
|
-
|
|
22
|
-
|
|
151
|
+
project_id: z.string().optional().describe('Optional project scope override for this call.'),
|
|
152
|
+
}, async ({ status, limit, project_id }) => {
|
|
153
|
+
const resolved = await resolveProject(project_id);
|
|
154
|
+
const { handoffs } = await api.listHandoffs(status, limit, resolved.projectId);
|
|
23
155
|
if (handoffs.length === 0) {
|
|
24
|
-
|
|
156
|
+
const scopeLabel = formatProjectScope(resolved.project);
|
|
157
|
+
return { content: [{ type: 'text', text: `No ${status} handoffs found in scope: ${scopeLabel}.` }] };
|
|
25
158
|
}
|
|
26
159
|
const text = handoffs.map((h) => {
|
|
27
160
|
let out = `## ${h.title}\n`;
|
|
28
161
|
out += `**Status:** ${h.status} · **Sources:** ${h.sources.join(', ') || 'all'}\n`;
|
|
162
|
+
if (h.project_name) {
|
|
163
|
+
out += `**Project:** ${h.project_name}\n`;
|
|
164
|
+
}
|
|
29
165
|
out += `**Created:** ${new Date(h.created_at).toLocaleString()}\n\n`;
|
|
30
166
|
out += `${h.summary}\n`;
|
|
31
167
|
if (h.key_changes?.length)
|
|
@@ -41,18 +177,23 @@ server.tool('list_handoffs', 'List your Flow Relay handoffs. Returns recent hand
|
|
|
41
177
|
return { content: [{ type: 'text', text }] };
|
|
42
178
|
});
|
|
43
179
|
// ── Tool: generate handoff ───────────────────────────────────────────
|
|
44
|
-
server.tool('generate_handoff', 'Generate a new context handoff
|
|
45
|
-
sources: z.array(
|
|
180
|
+
server.tool('generate_handoff', 'Generate a new context handoff for personal scope or a selected project. Supports per-source filters.', {
|
|
181
|
+
sources: z.array(SourceEnum)
|
|
46
182
|
.optional()
|
|
47
183
|
.describe('Specific sources to include (omit for all connected sources)'),
|
|
48
184
|
filters: z.record(z.string(), z.object({
|
|
49
185
|
projects: z.array(z.string()).optional().describe('Filter to specific repos, channels, projects, or teams'),
|
|
50
186
|
eventTypes: z.array(z.string()).optional().describe('Filter to specific event types (e.g. "push", "issue_created")'),
|
|
51
187
|
})).optional().describe('Per-source advanced filters'),
|
|
52
|
-
|
|
188
|
+
project_id: z.string().optional().describe('Optional project scope override for this call.'),
|
|
189
|
+
}, async ({ sources, filters, project_id }) => {
|
|
53
190
|
try {
|
|
54
|
-
const
|
|
191
|
+
const resolved = await resolveProject(project_id);
|
|
192
|
+
const { handoff } = await api.generateHandoff(sources, filters, resolved.projectId);
|
|
55
193
|
let text = `# ${handoff.title}\n\n${handoff.summary}\n`;
|
|
194
|
+
if (handoff.project_name) {
|
|
195
|
+
text += `\n**Project:** ${handoff.project_name}\n`;
|
|
196
|
+
}
|
|
56
197
|
text += `\n**Sources:** ${handoff.sources.join(', ')}\n`;
|
|
57
198
|
if (handoff.key_changes?.length)
|
|
58
199
|
text += `\n**Key changes:**\n${handoff.key_changes.map((c) => `- ${c}`).join('\n')}\n`;
|
|
@@ -67,31 +208,46 @@ server.tool('generate_handoff', 'Generate a new context handoff from your connec
|
|
|
67
208
|
}
|
|
68
209
|
});
|
|
69
210
|
// ── Tool: list integrations ─────────────────────────────────────────
|
|
70
|
-
server.tool('list_integrations', 'List
|
|
71
|
-
|
|
211
|
+
server.tool('list_integrations', 'List integrations in the current scope. In project scope this returns project resources with health metadata.', {
|
|
212
|
+
project_id: z.string().optional().describe('Optional project scope override for this call.'),
|
|
213
|
+
}, async ({ project_id }) => {
|
|
214
|
+
const resolved = await resolveProject(project_id);
|
|
215
|
+
const { integrations } = await api.listIntegrations(resolved.projectId);
|
|
72
216
|
if (integrations.length === 0) {
|
|
217
|
+
if (resolved.project) {
|
|
218
|
+
return { content: [{ type: 'text', text: `No integrations configured for project ${resolved.project.name}.` }] };
|
|
219
|
+
}
|
|
73
220
|
return { content: [{ type: 'text', text: 'No integrations connected. Visit https://www.flowrelay.it/integrations to set up.' }] };
|
|
74
221
|
}
|
|
75
222
|
const text = integrations.map((i) => {
|
|
76
223
|
const name = i.workspace_name ? ` (${i.workspace_name})` : '';
|
|
224
|
+
if (i.scope === 'project') {
|
|
225
|
+
const status = i.connection_status ?? 'unknown';
|
|
226
|
+
const providers = i.providers_connected ?? 0;
|
|
227
|
+
return `- **${i.source}**${name} — status: ${status}, providers connected: ${providers}`;
|
|
228
|
+
}
|
|
77
229
|
return `- **${i.source}**${name} — connected ${new Date(i.connected_at).toLocaleDateString()}`;
|
|
78
230
|
}).join('\n');
|
|
79
231
|
return { content: [{ type: 'text', text: `**Connected integrations:**\n${text}` }] };
|
|
80
232
|
});
|
|
81
233
|
// ── Tool: list recent events ─────────────────────────────────────────
|
|
82
|
-
server.tool('list_events', 'List recent context events
|
|
83
|
-
source:
|
|
234
|
+
server.tool('list_events', 'List recent context events in current scope (personal or selected project).', {
|
|
235
|
+
source: SourceEnum
|
|
84
236
|
.optional()
|
|
85
237
|
.describe('Filter by integration source'),
|
|
86
238
|
limit: z.number().min(1).max(100).default(20).describe('Max number of events'),
|
|
87
|
-
|
|
88
|
-
|
|
239
|
+
project_id: z.string().optional().describe('Optional project scope override for this call.'),
|
|
240
|
+
}, async ({ source, limit, project_id }) => {
|
|
241
|
+
const resolved = await resolveProject(project_id);
|
|
242
|
+
const { events } = await api.listEvents(source, limit, resolved.projectId);
|
|
89
243
|
if (events.length === 0) {
|
|
90
|
-
|
|
244
|
+
const scopeLabel = formatProjectScope(resolved.project);
|
|
245
|
+
return { content: [{ type: 'text', text: `No recent events${source ? ` from ${source}` : ''} in scope: ${scopeLabel}.` }] };
|
|
91
246
|
}
|
|
92
247
|
const text = events.map((e) => {
|
|
93
248
|
const time = new Date(e.created_at).toLocaleString();
|
|
94
|
-
|
|
249
|
+
const author = e.user_id ? ` user=${e.user_id}` : '';
|
|
250
|
+
return `- **[${e.source}/${e.event_type}]** ${e.title} _(${time})_${author}`;
|
|
95
251
|
}).join('\n');
|
|
96
252
|
return { content: [{ type: 'text', text: `**Recent events:**\n${text}` }] };
|
|
97
253
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowrelay/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Flow Relay MCP Server for Claude Desktop and Claude Code — handoffs, integrations, and context events via natural conversation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|