@flowrelay/mcp-server 0.8.0 → 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/README.md CHANGED
@@ -9,7 +9,7 @@ It connects to Flow Relay API v1 using an API key and supports:
9
9
  ## Package
10
10
 
11
11
  - Name: @flowrelay/mcp-server
12
- - Version: 0.8.0
12
+ - Version: 1.0.0
13
13
 
14
14
  ## What Is Included
15
15
 
@@ -18,15 +18,16 @@ The server currently exposes these tools:
18
18
  - get_workspace_context
19
19
  - list_projects
20
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
21
+ - list_filter_options (lists the selectable resources, branches, event types and priorities per source for a project call this before generating so handoff/insight filters use real values instead of guesses)
22
+ - list_handoffs (returns project-specific handoffs, or an aggregated view of all accessible project handoffs if no project is active; filter by status: active, archived, or all)
23
+ - generate_handoff (processed asynchronously, and the server automatically polls until the job finishes. Requires active project or project_id. Accepts per-source filters: projects, eventTypes, branches, priorities.)
24
+ - generate_correlation_insight (accepts per-source filters)
25
+ - generate_onboarding_brief (accepts per-source filters)
26
+ - generate_architecture_insight (accepts per-source filters)
26
27
  - list_insights
27
28
  - list_integrations
28
29
  - list_events
29
- - list_untracked_resources (lists event-producing resources not scoped to any project useful for discovering untracked activity)
30
+ - list_untracked_resources (lists event-producing resources not scoped to any project useful for discovering untracked activity)
30
31
  - discord_list_channels
31
32
  - discord_send_message
32
33
 
@@ -55,7 +56,7 @@ Add this to your claude_desktop_config.json:
55
56
  "args": ["-y", "@flowrelay/mcp-server"],
56
57
  "env": {
57
58
  "FLOWRELAY_API_KEY": "fr_your_api_key_here",
58
- "FLOWRELAY_PROJECT_ID": "optional_project_id"
59
+ "FLOWRELAY_PROJECT_ID": "your_project_id"
59
60
  }
60
61
  }
61
62
  }
package/dist/api.d.ts CHANGED
@@ -84,9 +84,13 @@ export interface UntrackedResource {
84
84
  resource_name: string;
85
85
  resource_type: string;
86
86
  }
87
+ export interface SourceFilter {
88
+ projects?: string[];
89
+ eventTypes?: string[];
90
+ branches?: string[];
91
+ priorities?: string[];
92
+ }
87
93
  export type GenerateHandoffResponse = {
88
- handoff: HandoffResult;
89
- } | {
90
94
  jobId: string;
91
95
  status: AiJobStatus;
92
96
  };
@@ -94,6 +98,30 @@ export type GenerateInsightResponse = {
94
98
  jobId: string;
95
99
  status: AiJobStatus;
96
100
  };
101
+ export interface AvailableSourceFilter {
102
+ projects: {
103
+ id: string;
104
+ label: string;
105
+ }[];
106
+ eventTypes: {
107
+ value: string;
108
+ label: string;
109
+ }[];
110
+ branches?: {
111
+ value: string;
112
+ label: string;
113
+ }[];
114
+ branchesByProject?: Record<string, {
115
+ value: string;
116
+ label: string;
117
+ }[]>;
118
+ defaultBranchByProject?: Record<string, string>;
119
+ priorities: {
120
+ value: string;
121
+ label: string;
122
+ }[];
123
+ }
124
+ export type AvailableFilters = Record<string, AvailableSourceFilter>;
97
125
  export declare class FlowRelayAPI {
98
126
  private baseUrl;
99
127
  private apiKey;
@@ -101,13 +129,11 @@ export declare class FlowRelayAPI {
101
129
  private requestRaw;
102
130
  private request;
103
131
  listProjects(): Promise<TenantContext>;
132
+ getHandoffFilters(projectId: string): Promise<AvailableFilters>;
104
133
  listHandoffs(status?: string, limit?: number, projectId?: string | null): Promise<{
105
134
  handoffs: HandoffResult[];
106
135
  }>;
107
- generateHandoff(sources: string[] | undefined, filters: Record<string, {
108
- projects?: string[];
109
- eventTypes?: string[];
110
- }> | undefined, projectId: string): Promise<GenerateHandoffResponse>;
136
+ generateHandoff(sources: string[] | undefined, filters: Record<string, SourceFilter> | undefined, projectId: string): Promise<GenerateHandoffResponse>;
111
137
  getJob(jobId: string): Promise<{
112
138
  job: AiJob;
113
139
  result: HandoffResult | InsightResult | null;
package/dist/api.js CHANGED
@@ -34,6 +34,10 @@ export class FlowRelayAPI {
34
34
  async listProjects() {
35
35
  return this.request('/projects');
36
36
  }
37
+ async getHandoffFilters(projectId) {
38
+ const { filters } = await this.request(`/handoffs/filters?project_id=${encodeURIComponent(projectId)}`);
39
+ return filters ?? {};
40
+ }
37
41
  async listHandoffs(status = 'active', limit = 10, projectId) {
38
42
  const params = new URLSearchParams();
39
43
  params.set('status', status);
@@ -49,13 +53,10 @@ export class FlowRelayAPI {
49
53
  if (filters && Object.keys(filters).length > 0)
50
54
  body.filters = filters;
51
55
  body.project_id = projectId;
52
- const { status, body: data } = await this.requestRaw('/handoffs', {
56
+ return this.request('/handoffs', {
53
57
  method: 'POST',
54
58
  body: JSON.stringify(body),
55
59
  });
56
- // 202 → async job; 200 → inline handoff. Caller branches on the shape.
57
- void status;
58
- return data;
59
60
  }
60
61
  async getJob(jobId) {
61
62
  return (await this.requestRaw(`/jobs/${jobId}`)).body;
package/dist/index.js CHANGED
@@ -21,6 +21,15 @@ const SOURCES = [
21
21
  'datadog',
22
22
  ];
23
23
  const SourceEnum = z.enum(SOURCES);
24
+ const SourceFilterSchema = z.object({
25
+ projects: z.array(z.string()).optional().describe('Filter to specific repos, channels, projects, or teams'),
26
+ eventTypes: z.array(z.string()).optional().describe('Filter to specific event types (e.g. "push", "issue_created")'),
27
+ branches: z.array(z.string()).optional().describe('Git branch names (e.g. "main", "develop") – versioning sources only'),
28
+ priorities: z.array(z.string()).optional().describe('Priority levels (e.g. "high", "urgent") – Jira, Linear, Sentry only'),
29
+ });
30
+ const FiltersSchema = z.record(z.string(), SourceFilterSchema)
31
+ .optional()
32
+ .describe('Per-source advanced filters, AND-combined across dimensions');
24
33
  function normalizeProjectId(value) {
25
34
  if (typeof value !== 'string')
26
35
  return null;
@@ -50,7 +59,7 @@ const api = new FlowRelayAPI(apiKey, process.env.FLOWRELAY_BASE_URL);
50
59
  let activeProjectId = normalizeProjectId(process.env.FLOWRELAY_PROJECT_ID);
51
60
  const server = new McpServer({
52
61
  name: 'flowrelay',
53
- version: '0.8.0',
62
+ version: '1.0.0',
54
63
  });
55
64
  async function getTenantContext() {
56
65
  const context = await api.listProjects();
@@ -160,7 +169,7 @@ server.tool('set_active_project', 'Set or clear the active project context used
160
169
  });
161
170
  // ── Tool: list handoffs ──────────────────────────────────────────────
162
171
  server.tool('list_handoffs', 'List Flow Relay handoffs in the current tenant scope (all accessible projects or selected project).', {
163
- status: z.enum(['active', 'archived', 'completed']).default('active').describe('Filter by status'),
172
+ status: z.enum(['active', 'archived', 'all']).default('active').describe('Filter by status ("all" returns every status)'),
164
173
  limit: z.number().min(1).max(50).default(10).describe('Max number of handoffs to return'),
165
174
  project_id: z.string().optional().describe('Optional project scope override for this call.'),
166
175
  }, async ({ status, limit, project_id }) => {
@@ -201,28 +210,18 @@ server.tool('generate_handoff', 'Generate a new context handoff for personal sco
201
210
  sources: z.array(SourceEnum)
202
211
  .optional()
203
212
  .describe('Specific sources to include (omit for all connected sources)'),
204
- filters: z.record(z.string(), z.object({
205
- projects: z.array(z.string()).optional().describe('Filter to specific repos, channels, projects, or teams'),
206
- eventTypes: z.array(z.string()).optional().describe('Filter to specific event types (e.g. "push", "issue_created")'),
207
- })).optional().describe('Per-source advanced filters'),
213
+ filters: FiltersSchema,
208
214
  project_id: z.string().optional().describe('Optional project scope override for this call.'),
209
215
  }, async ({ sources, filters, project_id }) => {
210
216
  try {
211
217
  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;
218
+ const { jobId } = await api.generateHandoff(sources, filters, resolved.projectId);
219
+ const { job, result } = await api.waitForJob(jobId);
220
+ if (job.status === 'failed' || !result) {
221
+ const reason = job.error ?? 'unknown error';
222
+ return { content: [{ type: 'text', text: `Could not generate handoff: ${reason}` }] };
225
223
  }
224
+ const handoff = result;
226
225
  let text = `# ${handoff.title}\n\n${handoff.summary}\n`;
227
226
  if (handoff.project_name)
228
227
  text += `\n**Project:** ${handoff.project_name}\n`;
@@ -343,12 +342,14 @@ server.tool('discord_send_message', 'Send a message to a Discord channel in your
343
342
  server.tool('generate_correlation_insight', 'Generate a cross-source correlation AI insight for the specified project scope.', {
344
343
  project_id: z.string().describe('The project ID scope to generate the correlation insight for.'),
345
344
  sources: z.array(SourceEnum).optional().describe('Filter events to specific sources (e.g. "github", "slack")'),
345
+ filters: FiltersSchema,
346
346
  lookback_hours: z.number().optional().describe('Number of hours of activity to analyze'),
347
347
  max_events: z.number().optional().describe('Maximum number of events to process'),
348
- }, async ({ project_id, sources, lookback_hours, max_events }) => {
348
+ }, async ({ project_id, sources, filters, lookback_hours, max_events }) => {
349
349
  try {
350
350
  const res = await api.generateInsight(project_id, 'correlation', {
351
351
  sources,
352
+ filters,
352
353
  lookbackHours: lookback_hours,
353
354
  maxEvents: max_events,
354
355
  });
@@ -368,14 +369,16 @@ server.tool('generate_correlation_insight', 'Generate a cross-source correlation
368
369
  server.tool('generate_onboarding_brief', 'Generate an onboarding brief AI insight for the specified project scope.', {
369
370
  project_id: z.string().describe('The project ID scope to generate the onboarding brief for.'),
370
371
  sources: z.array(SourceEnum).optional().describe('Filter events to specific sources'),
372
+ filters: FiltersSchema,
371
373
  new_member_role: z.string().optional().describe('Expected role/focus of the new team member'),
372
374
  focus_area: z.string().optional().describe('Specific repository or feature area they will work on'),
373
375
  lookback_days: z.number().optional().describe('Number of days of history to review'),
374
376
  max_events: z.number().optional().describe('Maximum events to process'),
375
- }, async ({ project_id, sources, new_member_role, focus_area, lookback_days, max_events }) => {
377
+ }, async ({ project_id, sources, filters, new_member_role, focus_area, lookback_days, max_events }) => {
376
378
  try {
377
379
  const res = await api.generateInsight(project_id, 'onboarding', {
378
380
  sources,
381
+ filters,
379
382
  newMemberRole: new_member_role,
380
383
  focusArea: focus_area,
381
384
  lookbackDays: lookback_days,
@@ -397,13 +400,15 @@ server.tool('generate_onboarding_brief', 'Generate an onboarding brief AI insigh
397
400
  server.tool('generate_architecture_insight', 'Generate an architecture insight AI insight for the specified project scope.', {
398
401
  project_id: z.string().describe('The project ID scope to generate the architecture insight for.'),
399
402
  sources: z.array(SourceEnum).optional().describe('Filter events to specific sources'),
403
+ filters: FiltersSchema,
400
404
  focus_question: z.string().optional().describe('Specific architectural question or component to focus on'),
401
405
  lookback_days: z.number().optional().describe('Number of days of history to review'),
402
406
  max_events: z.number().optional().describe('Maximum events to process'),
403
- }, async ({ project_id, sources, focus_question, lookback_days, max_events }) => {
407
+ }, async ({ project_id, sources, filters, focus_question, lookback_days, max_events }) => {
404
408
  try {
405
409
  const res = await api.generateInsight(project_id, 'architecture', {
406
410
  sources,
411
+ filters,
407
412
  focusQuestion: focus_question,
408
413
  lookbackDays: lookback_days,
409
414
  maxEvents: max_events,
@@ -424,7 +429,7 @@ server.tool('generate_architecture_insight', 'Generate an architecture insight A
424
429
  server.tool('list_insights', 'List project AI insights for a selected project scope.', {
425
430
  project_id: z.string().describe('The project ID context to list insights for.'),
426
431
  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'),
432
+ status: z.enum(['active', 'archived', 'all']).default('active').describe('Filter by status ("all" returns every status)'),
428
433
  limit: z.number().min(1).max(50).default(20).describe('Max number of insights to return'),
429
434
  }, async ({ project_id, kind, status, limit }) => {
430
435
  try {
@@ -441,6 +446,39 @@ server.tool('list_insights', 'List project AI insights for a selected project sc
441
446
  return { content: [{ type: 'text', text: `Failed to list insights: ${err.message}` }] };
442
447
  }
443
448
  });
449
+ // ── Tool: list filter options ───────────────────────────────────────
450
+ server.tool('list_filter_options', 'List the real per-source filter values (resources, branches, event types, priorities) available for a project. Call this BEFORE generate_handoff or any generate_*_insight so the "filters" argument uses real values instead of guesses. Pass a resource id into filters[source].projects, a branch name into .branches, an event type into .eventTypes, a priority into .priorities.', {
451
+ project_id: z.string().optional().describe('Project scope. Omit to use the active project.'),
452
+ }, async ({ project_id }) => {
453
+ try {
454
+ const resolved = await requireProject(project_id);
455
+ const filters = await api.getHandoffFilters(resolved.projectId);
456
+ const sources = Object.keys(filters);
457
+ if (sources.length === 0) {
458
+ return { content: [{ type: 'text', text: `No connected sources with filter options for ${resolved.project.name}.` }] };
459
+ }
460
+ const blocks = sources.map((src) => {
461
+ const f = filters[src];
462
+ const lines = [`### ${src}`];
463
+ if (f.projects?.length) {
464
+ const items = f.projects.slice(0, 50).map((p) => (p.label && p.label !== p.id ? `${p.label} (id: ${p.id})` : p.id));
465
+ const more = f.projects.length > 50 ? ` (+${f.projects.length - 50} more)` : '';
466
+ lines.push(`- Resources: ${items.join(', ')}${more}`);
467
+ }
468
+ if (f.branches?.length)
469
+ lines.push(`- Branches: ${f.branches.map((b) => b.value).join(', ')}`);
470
+ if (f.eventTypes?.length)
471
+ lines.push(`- Event types: ${f.eventTypes.map((e) => e.value).join(', ')}`);
472
+ if (f.priorities?.length)
473
+ lines.push(`- Priorities: ${f.priorities.map((p) => p.value).join(', ')}`);
474
+ return lines.join('\n');
475
+ });
476
+ return { content: [{ type: 'text', text: `Filter options for ${resolved.project.name}:\n\n${blocks.join('\n\n')}` }] };
477
+ }
478
+ catch (err) {
479
+ return { content: [{ type: 'text', text: `Could not load filter options: ${err.message}` }] };
480
+ }
481
+ });
444
482
  // ── Start ────────────────────────────────────────────────────────────
445
483
  const transport = new StdioServerTransport();
446
484
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowrelay/mcp-server",
3
- "version": "0.8.0",
3
+ "version": "1.0.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",