@flowrelay/mcp-server 0.4.1 → 0.8.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 CHANGED
@@ -1,101 +1,104 @@
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.1
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
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:
6
+
7
+ - Project scope (personal project or organization project). Every handoff and AI insight is tied to a project. Events and integrations remain user-level.
8
+
9
+ ## Package
10
+
11
+ - Name: @flowrelay/mcp-server
12
+ - Version: 0.8.0
13
+
14
+ ## What Is Included
15
+
16
+ The server currently exposes these tools:
17
+
18
+ - get_workspace_context
19
+ - list_projects
20
+ - set_active_project
21
+ - list_handoffs (returns project-specific handoffs, or an aggregated view of all accessible project handoffs if no project is active)
22
+ - generate_handoff (processed asynchronously, and the server automatically polls until the job finishes. Requires active project or project_id.)
23
+ - generate_correlation_insight
24
+ - generate_onboarding_brief
25
+ - generate_architecture_insight
26
+ - list_insights
27
+ - list_integrations
28
+ - list_events
29
+ - list_untracked_resources (lists event-producing resources not scoped to any project — useful for discovering untracked activity)
30
+ - discord_list_channels
31
+ - discord_send_message
32
+
33
+ ## Environment Variables
34
+
35
+ Required:
36
+
37
+ - FLOWRELAY_API_KEY
38
+
39
+ Optional:
40
+
41
+ - FLOWRELAY_PROJECT_ID
42
+ - FLOWRELAY_BASE_URL
43
+
44
+ If FLOWRELAY_PROJECT_ID is set, it becomes the default project context for project-aware tools unless you override it per call.
45
+
46
+ ## Quick Start (Claude Desktop)
47
+
48
+ Add this to your claude_desktop_config.json:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "flowrelay": {
54
+ "command": "npx",
55
+ "args": ["-y", "@flowrelay/mcp-server"],
56
+ "env": {
57
+ "FLOWRELAY_API_KEY": "fr_your_api_key_here",
58
+ "FLOWRELAY_PROJECT_ID": "optional_project_id"
59
+ }
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Multi-Tenant Behavior
66
+
67
+ - Every handoff and insight is tied to a project; you need an active project (`set_active_project`) or `project_id`. Events and integrations remain user-level.
68
+ - You can select a project during the MCP session with set_active_project.
69
+ - You can override scope per call by passing project_id where supported.
70
+
71
+ Recommended flow:
72
+
73
+ 1. Call get_workspace_context
74
+ 2. Call list_projects
75
+ 3. Call set_active_project
76
+ 4. Run handoff and query tools in the selected scope
77
+
78
+ ## Local Development
79
+
80
+ From this folder:
81
+
82
+ ```bash
83
+ npm install
84
+ npm run build
85
+ ```
86
+
87
+ Create a tarball package:
88
+
89
+ ```bash
90
+ npm pack
91
+ ```
92
+
93
+ ## Troubleshooting
94
+
95
+ - Error: Missing FLOWRELAY_API_KEY
96
+ - Set FLOWRELAY_API_KEY in your MCP client configuration.
97
+ - Project not found or inaccessible
98
+ - Run list_projects and use one of the returned IDs.
99
+ - No events or handoffs returned
100
+ - Verify active scope and data availability in that scope.
101
+
102
+ ## Related Docs
103
+
104
+ - Repository overview: ../README.md
package/dist/api.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Flow Relay API client talks to flowrelay.it/api/v1/*
2
+ * Flow Relay API client talks to flowrelay.it/api/v1/*
3
3
  */
4
4
  export type AccountType = 'personal' | 'business';
5
5
  export type AccessRole = 'owner' | 'admin' | 'member';
@@ -28,51 +28,101 @@ export interface TenantContext {
28
28
  organizations: TenantOrganization[];
29
29
  projects: TenantProject[];
30
30
  }
31
+ export type AiJobStatus = 'pending' | 'processing' | 'completed' | 'failed';
32
+ export interface AiJob {
33
+ id: string;
34
+ project_id: string;
35
+ kind: string;
36
+ status: AiJobStatus;
37
+ result_kind: 'handoff' | 'insight' | null;
38
+ result_id: string | null;
39
+ error: string | null;
40
+ error_code: string | null;
41
+ error_meta: Record<string, unknown>;
42
+ created_at: string;
43
+ updated_at: string;
44
+ }
45
+ export interface HandoffResult {
46
+ id: string;
47
+ user_id: string;
48
+ project_id: string | null;
49
+ project_name?: string | null;
50
+ scope_type?: 'personal' | 'project';
51
+ title: string;
52
+ summary: string;
53
+ status: string;
54
+ sources: string[];
55
+ key_changes?: string[];
56
+ decisions: string[];
57
+ open_questions: string[];
58
+ next_steps: string[];
59
+ created_at: string;
60
+ updated_at?: string;
61
+ }
62
+ export interface InsightResult {
63
+ id: string;
64
+ project_id: string;
65
+ requested_by: string;
66
+ kind: 'onboarding_brief' | 'cross_source_correlation' | 'architecture_insight';
67
+ title: string;
68
+ summary: string;
69
+ data: Record<string, unknown>;
70
+ model_used: string;
71
+ token_usage: {
72
+ prompt_tokens: number;
73
+ completion_tokens: number;
74
+ total_tokens: number;
75
+ };
76
+ status: string;
77
+ related_event_ids: string[];
78
+ created_at: string;
79
+ updated_at?: string;
80
+ }
81
+ export interface UntrackedResource {
82
+ source: string;
83
+ resource_id: string;
84
+ resource_name: string;
85
+ resource_type: string;
86
+ }
87
+ export type GenerateHandoffResponse = {
88
+ handoff: HandoffResult;
89
+ } | {
90
+ jobId: string;
91
+ status: AiJobStatus;
92
+ };
93
+ export type GenerateInsightResponse = {
94
+ jobId: string;
95
+ status: AiJobStatus;
96
+ };
31
97
  export declare class FlowRelayAPI {
32
98
  private baseUrl;
33
99
  private apiKey;
34
100
  constructor(apiKey: string, baseUrl?: string);
101
+ private requestRaw;
35
102
  private request;
36
103
  listProjects(): Promise<TenantContext>;
37
104
  listHandoffs(status?: string, limit?: number, projectId?: string | null): Promise<{
38
- handoffs: Array<{
39
- id: string;
40
- user_id: string;
41
- project_id: string | null;
42
- project_name?: string | null;
43
- scope_type?: "personal" | "project";
44
- title: string;
45
- summary: string;
46
- status: string;
47
- sources: string[];
48
- key_changes?: string[];
49
- decisions: string[];
50
- open_questions: string[];
51
- next_steps: string[];
52
- created_at: string;
53
- updated_at: string;
54
- }>;
105
+ handoffs: HandoffResult[];
55
106
  }>;
56
- generateHandoff(sources?: string[], filters?: Record<string, {
107
+ generateHandoff(sources: string[] | undefined, filters: Record<string, {
57
108
  projects?: string[];
58
109
  eventTypes?: string[];
59
- }>, projectId?: string | null): Promise<{
60
- handoff: {
61
- id: string;
62
- user_id: string;
63
- project_id: string | null;
64
- project_name?: string | null;
65
- scope_type?: "personal" | "project";
66
- title: string;
67
- summary: string;
68
- sources: string[];
69
- key_changes?: string[];
70
- decisions: string[];
71
- open_questions: string[];
72
- next_steps: string[];
73
- created_at: string;
74
- };
110
+ }> | undefined, projectId: string): Promise<GenerateHandoffResponse>;
111
+ getJob(jobId: string): Promise<{
112
+ job: AiJob;
113
+ result: HandoffResult | InsightResult | null;
114
+ }>;
115
+ waitForJob(jobId: string, opts?: {
116
+ intervalMs?: number;
117
+ timeoutMs?: number;
118
+ }): Promise<{
119
+ job: AiJob;
120
+ result: HandoffResult | InsightResult | null;
121
+ }>;
122
+ listInsights(projectId: string, kind?: string, status?: string, limit?: number): Promise<{
123
+ insights: InsightResult[];
75
124
  }>;
125
+ generateInsight(projectId: string, kind: 'correlation' | 'onboarding' | 'architecture', body?: Record<string, unknown>): Promise<GenerateInsightResponse>;
76
126
  listIntegrations(projectId?: string | null): Promise<{
77
127
  integrations: Array<{
78
128
  source: string;
@@ -108,4 +158,5 @@ export declare class FlowRelayAPI {
108
158
  ok: boolean;
109
159
  message_id: string;
110
160
  }>;
161
+ listUntrackedResources(): Promise<UntrackedResource[]>;
111
162
  }
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Flow Relay API client talks to flowrelay.it/api/v1/*
2
+ * Flow Relay API client talks to flowrelay.it/api/v1/*
3
3
  */
4
4
  const DEFAULT_BASE_URL = 'https://www.flowrelay.it';
5
5
  export class FlowRelayAPI {
@@ -9,7 +9,7 @@ export class FlowRelayAPI {
9
9
  this.apiKey = apiKey;
10
10
  this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;
11
11
  }
12
- async request(path, options) {
12
+ async requestRaw(path, options) {
13
13
  const url = `${this.baseUrl}/api/v1${path}`;
14
14
  const res = await fetch(url, {
15
15
  ...options,
@@ -23,10 +23,13 @@ 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
- }
29
- return res.json();
26
+ if (res.status === 204)
27
+ return { status: 204, body: {} };
28
+ return { status: res.status, body: (await res.json()) };
29
+ }
30
+ async request(path, options) {
31
+ const { body } = await this.requestRaw(path, options);
32
+ return body;
30
33
  }
31
34
  async listProjects() {
32
35
  return this.request('/projects');
@@ -45,12 +48,46 @@ export class FlowRelayAPI {
45
48
  body.sources = sources;
46
49
  if (filters && Object.keys(filters).length > 0)
47
50
  body.filters = filters;
48
- if (projectId)
49
- body.project_id = projectId;
50
- return this.request('/handoffs', {
51
+ body.project_id = projectId;
52
+ const { status, body: data } = await this.requestRaw('/handoffs', {
51
53
  method: 'POST',
52
54
  body: JSON.stringify(body),
53
55
  });
56
+ // 202 → async job; 200 → inline handoff. Caller branches on the shape.
57
+ void status;
58
+ return data;
59
+ }
60
+ async getJob(jobId) {
61
+ return (await this.requestRaw(`/jobs/${jobId}`)).body;
62
+ }
63
+ async waitForJob(jobId, opts = {}) {
64
+ const intervalMs = opts.intervalMs ?? 2500;
65
+ const timeoutMs = opts.timeoutMs ?? 180_000; // 3 min hard cap
66
+ const deadline = Date.now() + timeoutMs;
67
+ while (true) {
68
+ const res = await this.getJob(jobId);
69
+ if (res.job.status === 'completed' || res.job.status === 'failed') {
70
+ return res;
71
+ }
72
+ if (Date.now() > deadline) {
73
+ throw new Error(`AI Job timed out after ${Math.round(timeoutMs / 1000)}s (job ${jobId} still ${res.job.status}).`);
74
+ }
75
+ await new Promise((r) => setTimeout(r, intervalMs));
76
+ }
77
+ }
78
+ async listInsights(projectId, kind, status = 'active', limit = 20) {
79
+ const params = new URLSearchParams();
80
+ if (kind)
81
+ params.set('kind', kind);
82
+ params.set('status', status);
83
+ params.set('limit', String(limit));
84
+ return this.request(`/projects/${projectId}/insights?${params.toString()}`);
85
+ }
86
+ async generateInsight(projectId, kind, body) {
87
+ return this.request(`/projects/${projectId}/insights/${kind}`, {
88
+ method: 'POST',
89
+ body: JSON.stringify(body ?? {}),
90
+ });
54
91
  }
55
92
  async listIntegrations(projectId) {
56
93
  const params = new URLSearchParams();
@@ -77,4 +114,7 @@ export class FlowRelayAPI {
77
114
  body: JSON.stringify({ channel_id: channelId, content }),
78
115
  });
79
116
  }
117
+ async listUntrackedResources() {
118
+ return this.request('/integrations/untracked');
119
+ }
80
120
  }
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ function normalizeProjectId(value) {
29
29
  }
30
30
  function formatProjectScope(project) {
31
31
  if (!project)
32
- return 'No project (personal stream)';
32
+ return 'No project selected (required for handoffs & insights)';
33
33
  if (project.project_type === 'personal') {
34
34
  return `Personal project: ${project.name}`;
35
35
  }
@@ -50,7 +50,7 @@ const api = new FlowRelayAPI(apiKey, process.env.FLOWRELAY_BASE_URL);
50
50
  let activeProjectId = normalizeProjectId(process.env.FLOWRELAY_PROJECT_ID);
51
51
  const server = new McpServer({
52
52
  name: 'flowrelay',
53
- version: '0.4.1',
53
+ version: '0.8.0',
54
54
  });
55
55
  async function getTenantContext() {
56
56
  const context = await api.listProjects();
@@ -73,6 +73,20 @@ async function resolveProject(projectId) {
73
73
  }
74
74
  return { projectId: resolvedProjectId, project };
75
75
  }
76
+ async function requireProject(projectId) {
77
+ const explicitProjectId = normalizeProjectId(projectId);
78
+ const resolvedProjectId = explicitProjectId ?? activeProjectId;
79
+ if (!resolvedProjectId) {
80
+ throw new Error('A project is required. Use set_active_project or pass project_id. Run list_projects to see available projects.');
81
+ }
82
+ const context = await getTenantContext();
83
+ const project = context.projects.find((candidate) => candidate.id === resolvedProjectId) ?? null;
84
+ if (!project) {
85
+ activeProjectId = null;
86
+ throw new Error('A project is required. The selected project was not found or is inaccessible. Use set_active_project or pass project_id. Run list_projects to see available projects.');
87
+ }
88
+ return { projectId: resolvedProjectId, project };
89
+ }
76
90
  // ── Tool: workspace context ─────────────────────────────────────────
77
91
  server.tool('get_workspace_context', 'Show Flow Relay tenant context: personal/business mode, active project scope, and current role.', {}, async () => {
78
92
  const context = await getTenantContext();
@@ -99,7 +113,7 @@ server.tool('list_projects', 'List all projects available to this API key, inclu
99
113
  return {
100
114
  content: [{
101
115
  type: 'text',
102
- text: 'No accessible projects found. Use personal stream or get added to a project team.',
116
+ text: 'No accessible projects found. Create a project in Flow Relay or get added to a project team.',
103
117
  }],
104
118
  };
105
119
  }
@@ -124,7 +138,7 @@ server.tool('set_active_project', 'Set or clear the active project context used
124
138
  }, async ({ project_id, clear }) => {
125
139
  if (clear || !normalizeProjectId(project_id)) {
126
140
  activeProjectId = null;
127
- return { content: [{ type: 'text', text: 'Active project cleared. Using no-project personal scope.' }] };
141
+ return { content: [{ type: 'text', text: 'Active project cleared. Handoff/insight generation requires selecting a project.' }] };
128
142
  }
129
143
  const context = await getTenantContext();
130
144
  const selected = context.projects.find((project) => project.id === project_id);
@@ -145,23 +159,29 @@ server.tool('set_active_project', 'Set or clear the active project context used
145
159
  };
146
160
  });
147
161
  // ── Tool: list handoffs ──────────────────────────────────────────────
148
- server.tool('list_handoffs', 'List Flow Relay handoffs in the current tenant scope (personal stream or selected project).', {
162
+ server.tool('list_handoffs', 'List Flow Relay handoffs in the current tenant scope (all accessible projects or selected project).', {
149
163
  status: z.enum(['active', 'archived', 'completed']).default('active').describe('Filter by status'),
150
164
  limit: z.number().min(1).max(50).default(10).describe('Max number of handoffs to return'),
151
165
  project_id: z.string().optional().describe('Optional project scope override for this call.'),
152
166
  }, async ({ status, limit, project_id }) => {
153
- const resolved = await resolveProject(project_id);
154
- const { handoffs } = await api.listHandoffs(status, limit, resolved.projectId);
167
+ const explicitProjectId = normalizeProjectId(project_id);
168
+ const resolvedProjectId = explicitProjectId ?? activeProjectId;
169
+ let resolvedProject = null;
170
+ if (resolvedProjectId) {
171
+ const context = await getTenantContext();
172
+ resolvedProject = context.projects.find((p) => p.id === resolvedProjectId) ?? null;
173
+ if (!resolvedProject)
174
+ activeProjectId = null;
175
+ }
176
+ const { handoffs } = await api.listHandoffs(status, limit, resolvedProject?.id ?? null);
155
177
  if (handoffs.length === 0) {
156
- const scopeLabel = formatProjectScope(resolved.project);
178
+ const scopeLabel = resolvedProject ? formatProjectScope(resolvedProject) : 'all projects';
157
179
  return { content: [{ type: 'text', text: `No ${status} handoffs found in scope: ${scopeLabel}.` }] };
158
180
  }
159
181
  const text = handoffs.map((h) => {
160
182
  let out = `## ${h.title}\n`;
161
183
  out += `**Status:** ${h.status} · **Sources:** ${h.sources.join(', ') || 'all'}\n`;
162
- if (h.project_name) {
163
- out += `**Project:** ${h.project_name}\n`;
164
- }
184
+ out += `**Project:** ${h.project_name ?? 'Unknown'}\n`;
165
185
  out += `**Created:** ${new Date(h.created_at).toLocaleString()}\n\n`;
166
186
  out += `${h.summary}\n`;
167
187
  if (h.key_changes?.length)
@@ -188,12 +208,24 @@ server.tool('generate_handoff', 'Generate a new context handoff for personal sco
188
208
  project_id: z.string().optional().describe('Optional project scope override for this call.'),
189
209
  }, async ({ sources, filters, project_id }) => {
190
210
  try {
191
- const resolved = await resolveProject(project_id);
192
- const { handoff } = await api.generateHandoff(sources, filters, resolved.projectId);
211
+ const resolved = await requireProject(project_id);
212
+ const res = await api.generateHandoff(sources, filters, resolved.projectId);
213
+ // Async path (project handoff): poll until the worker finishes.
214
+ let handoff;
215
+ if ('jobId' in res) {
216
+ const { job, result } = await api.waitForJob(res.jobId);
217
+ if (job.status === 'failed' || !result) {
218
+ const reason = job.error ?? 'unknown error';
219
+ return { content: [{ type: 'text', text: `Could not generate handoff: ${reason}` }] };
220
+ }
221
+ handoff = result;
222
+ }
223
+ else {
224
+ handoff = res.handoff;
225
+ }
193
226
  let text = `# ${handoff.title}\n\n${handoff.summary}\n`;
194
- if (handoff.project_name) {
227
+ if (handoff.project_name)
195
228
  text += `\n**Project:** ${handoff.project_name}\n`;
196
- }
197
229
  text += `\n**Sources:** ${handoff.sources.join(', ')}\n`;
198
230
  if (handoff.key_changes?.length)
199
231
  text += `\n**Key changes:**\n${handoff.key_changes.map((c) => `- ${c}`).join('\n')}\n`;
@@ -224,12 +256,38 @@ server.tool('list_integrations', 'List integrations in the current scope. In pro
224
256
  if (i.scope === 'project') {
225
257
  const status = i.connection_status ?? 'unknown';
226
258
  const providers = i.providers_connected ?? 0;
227
- return `- **${i.source}**${name} status: ${status}, providers connected: ${providers}`;
259
+ return `- **${i.source}**${name} status: ${status}, providers connected: ${providers}`;
228
260
  }
229
- return `- **${i.source}**${name} connected ${new Date(i.connected_at).toLocaleDateString()}`;
261
+ return `- **${i.source}**${name} connected ${new Date(i.connected_at).toLocaleDateString()}`;
230
262
  }).join('\n');
231
263
  return { content: [{ type: 'text', text: `**Connected integrations:**\n${text}` }] };
232
264
  });
265
+ // ── Tool: list untracked resources ───────────────────────────────────
266
+ server.tool('list_untracked_resources', 'List discovered active resources across all connected user integrations that have not yet been assigned to any project.', {}, async () => {
267
+ try {
268
+ const resources = await api.listUntrackedResources();
269
+ if (resources.length === 0) {
270
+ return {
271
+ content: [{
272
+ type: 'text',
273
+ text: 'All discovered active resources are already tracked in your projects. Good job!',
274
+ }],
275
+ };
276
+ }
277
+ const text = resources.map((r) => {
278
+ return `- **[${r.source}]** ${r.resource_name} (type: ${r.resource_type}, id: ${r.resource_id})`;
279
+ }).join('\n');
280
+ return {
281
+ content: [{
282
+ type: 'text',
283
+ text: `**Untracked active resources:**\n${text}\n\n*Note: Map these resources to Flow Relay projects in the web dashboard or CLI to start tracking their events.*`,
284
+ }],
285
+ };
286
+ }
287
+ catch (err) {
288
+ return { content: [{ type: 'text', text: `Failed to list untracked resources: ${err.message}` }] };
289
+ }
290
+ });
233
291
  // ── Tool: list recent events ─────────────────────────────────────────
234
292
  server.tool('list_events', 'List recent context events in current scope (personal or selected project).', {
235
293
  source: SourceEnum
@@ -259,7 +317,7 @@ server.tool('discord_list_channels', 'List text channels in your connected Disco
259
317
  return { content: [{ type: 'text', text: 'No text channels found in the connected Discord server.' }] };
260
318
  }
261
319
  const text = channels.map((ch) => {
262
- const topic = ch.topic ? ` ${ch.topic}` : '';
320
+ const topic = ch.topic ? ` ${ch.topic}` : '';
263
321
  return `- **#${ch.name}** (${ch.id})${topic}`;
264
322
  }).join('\n');
265
323
  return { content: [{ type: 'text', text: `**Discord channels:**\n${text}` }] };
@@ -281,6 +339,108 @@ server.tool('discord_send_message', 'Send a message to a Discord channel in your
281
339
  return { content: [{ type: 'text', text: `Failed to send message: ${err.message}` }] };
282
340
  }
283
341
  });
342
+ // ── Tool: generate correlation insight ──────────────────────────────
343
+ server.tool('generate_correlation_insight', 'Generate a cross-source correlation AI insight for the specified project scope.', {
344
+ project_id: z.string().describe('The project ID scope to generate the correlation insight for.'),
345
+ sources: z.array(SourceEnum).optional().describe('Filter events to specific sources (e.g. "github", "slack")'),
346
+ lookback_hours: z.number().optional().describe('Number of hours of activity to analyze'),
347
+ max_events: z.number().optional().describe('Maximum number of events to process'),
348
+ }, async ({ project_id, sources, lookback_hours, max_events }) => {
349
+ try {
350
+ const res = await api.generateInsight(project_id, 'correlation', {
351
+ sources,
352
+ lookbackHours: lookback_hours,
353
+ maxEvents: max_events,
354
+ });
355
+ const { job, result } = await api.waitForJob(res.jobId);
356
+ if (job.status === 'failed' || !result) {
357
+ const reason = job.error ?? 'unknown error';
358
+ return { content: [{ type: 'text', text: `Could not generate correlation insight: ${reason}` }] };
359
+ }
360
+ const insight = result;
361
+ return { content: [{ type: 'text', text: `# ${insight.title}\n\n${insight.summary}` }] };
362
+ }
363
+ catch (err) {
364
+ return { content: [{ type: 'text', text: `Failed: ${err.message}` }] };
365
+ }
366
+ });
367
+ // ── Tool: generate onboarding brief ──────────────────────────────────
368
+ server.tool('generate_onboarding_brief', 'Generate an onboarding brief AI insight for the specified project scope.', {
369
+ project_id: z.string().describe('The project ID scope to generate the onboarding brief for.'),
370
+ sources: z.array(SourceEnum).optional().describe('Filter events to specific sources'),
371
+ new_member_role: z.string().optional().describe('Expected role/focus of the new team member'),
372
+ focus_area: z.string().optional().describe('Specific repository or feature area they will work on'),
373
+ lookback_days: z.number().optional().describe('Number of days of history to review'),
374
+ max_events: z.number().optional().describe('Maximum events to process'),
375
+ }, async ({ project_id, sources, new_member_role, focus_area, lookback_days, max_events }) => {
376
+ try {
377
+ const res = await api.generateInsight(project_id, 'onboarding', {
378
+ sources,
379
+ newMemberRole: new_member_role,
380
+ focusArea: focus_area,
381
+ lookbackDays: lookback_days,
382
+ maxEvents: max_events,
383
+ });
384
+ const { job, result } = await api.waitForJob(res.jobId);
385
+ if (job.status === 'failed' || !result) {
386
+ const reason = job.error ?? 'unknown error';
387
+ return { content: [{ type: 'text', text: `Could not generate onboarding brief: ${reason}` }] };
388
+ }
389
+ const insight = result;
390
+ return { content: [{ type: 'text', text: `# ${insight.title}\n\n${insight.summary}` }] };
391
+ }
392
+ catch (err) {
393
+ return { content: [{ type: 'text', text: `Failed: ${err.message}` }] };
394
+ }
395
+ });
396
+ // ── Tool: generate architecture insight ─────────────────────────────
397
+ server.tool('generate_architecture_insight', 'Generate an architecture insight AI insight for the specified project scope.', {
398
+ project_id: z.string().describe('The project ID scope to generate the architecture insight for.'),
399
+ sources: z.array(SourceEnum).optional().describe('Filter events to specific sources'),
400
+ focus_question: z.string().optional().describe('Specific architectural question or component to focus on'),
401
+ lookback_days: z.number().optional().describe('Number of days of history to review'),
402
+ max_events: z.number().optional().describe('Maximum events to process'),
403
+ }, async ({ project_id, sources, focus_question, lookback_days, max_events }) => {
404
+ try {
405
+ const res = await api.generateInsight(project_id, 'architecture', {
406
+ sources,
407
+ focusQuestion: focus_question,
408
+ lookbackDays: lookback_days,
409
+ maxEvents: max_events,
410
+ });
411
+ const { job, result } = await api.waitForJob(res.jobId);
412
+ if (job.status === 'failed' || !result) {
413
+ const reason = job.error ?? 'unknown error';
414
+ return { content: [{ type: 'text', text: `Could not generate architecture insight: ${reason}` }] };
415
+ }
416
+ const insight = result;
417
+ return { content: [{ type: 'text', text: `# ${insight.title}\n\n${insight.summary}` }] };
418
+ }
419
+ catch (err) {
420
+ return { content: [{ type: 'text', text: `Failed: ${err.message}` }] };
421
+ }
422
+ });
423
+ // ── Tool: list insights ─────────────────────────────────────────────
424
+ server.tool('list_insights', 'List project AI insights for a selected project scope.', {
425
+ project_id: z.string().describe('The project ID context to list insights for.'),
426
+ kind: z.enum(['onboarding_brief', 'cross_source_correlation', 'architecture_insight']).optional().describe('Filter by insight kind'),
427
+ status: z.enum(['active', 'archived', 'completed']).default('active').describe('Filter by status'),
428
+ limit: z.number().min(1).max(50).default(20).describe('Max number of insights to return'),
429
+ }, async ({ project_id, kind, status, limit }) => {
430
+ try {
431
+ const { insights } = await api.listInsights(project_id, kind, status, limit);
432
+ if (insights.length === 0) {
433
+ return { content: [{ type: 'text', text: `No ${status} insights found.` }] };
434
+ }
435
+ const text = insights.map((insight) => {
436
+ return `## ${insight.title} (${insight.kind})\n**Status:** ${insight.status} · **Created:** ${new Date(insight.created_at).toLocaleString()}\n\n${insight.summary}`;
437
+ }).join('\n---\n\n');
438
+ return { content: [{ type: 'text', text }] };
439
+ }
440
+ catch (err) {
441
+ return { content: [{ type: 'text', text: `Failed to list insights: ${err.message}` }] };
442
+ }
443
+ });
284
444
  // ── Start ────────────────────────────────────────────────────────────
285
445
  const transport = new StdioServerTransport();
286
446
  await server.connect(transport);
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@flowrelay/mcp-server",
3
- "version": "0.4.1",
4
- "description": "Flow Relay MCP Server for Claude Desktop and Claude Code handoffs, integrations, and context events via natural conversation.",
3
+ "version": "0.8.0",
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",
7
7
  "author": "Adriano Sorbello",
8
8
  "homepage": "https://www.flowrelay.it",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/atrisorb/flow-relay.git"
11
+ "url": "git+https://github.com/atrisorb/flow-relay.git"
12
12
  },
13
13
  "keywords": [
14
14
  "mcp",
@@ -19,7 +19,7 @@
19
19
  "ai"
20
20
  ],
21
21
  "bin": {
22
- "flowrelay-mcp": "./dist/index.js"
22
+ "flowrelay-mcp": "dist/index.js"
23
23
  },
24
24
  "files": [
25
25
  "dist"