Skip to content

Commit 193dbe6

Browse files
committed
added webhook and indexing
1 parent 0ab23a6 commit 193dbe6

11 files changed

Lines changed: 547 additions & 219 deletions

File tree

grain/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The Grain MCP provides seamless integration with [Grain](https://grain.com), all
1313
- 📊 **Rich Metadata**: Access titles, dates, durations, participants, and summaries
1414
- 🎯 **Filter by Status**: View only ready recordings or check processing status
1515
- 📅 **Date Range Filtering**: Find meetings within specific time periods
16+
- 🔔 **Real-time Webhooks**: Receive automatic notifications when recordings are created, updated, or processed
1617

1718
## Authentication
1819

@@ -121,6 +122,31 @@ Each recording object includes:
121122
}
122123
```
123124

125+
## Webhooks
126+
127+
This MCP automatically sets up webhooks with Grain to receive real-time notifications about your recordings. When you install or configure this MCP, it will:
128+
129+
1. **Automatically create webhooks** pointing to the Deco Mesh
130+
2. **Listen for events** such as:
131+
- `recording.created` - When a new recording starts
132+
- `recording.updated` - When recording metadata changes
133+
- `recording.processed` - When transcription and AI processing completes
134+
135+
3. **Process events** through the `eventHandler` where you can add custom logic
136+
137+
### How Webhooks Work
138+
139+
```
140+
Grain Event → Grain API → Mesh → Your MCP → Custom Logic
141+
```
142+
143+
The webhook URL is automatically constructed as:
144+
```
145+
${meshUrl}/events/grain_recording?sub=${connectionId}
146+
```
147+
148+
This ensures events are routed to your specific MCP instance.
149+
124150
## Use Cases
125151

126152
### Meeting Analytics
@@ -138,6 +164,9 @@ Identify meetings that require follow-up based on participants, topics, or actio
138164
### Team Insights
139165
Track team collaboration by analyzing who attends which meetings and how often different people interact.
140166

167+
### Real-time Notifications
168+
Get instant alerts when new recordings are ready, enabling immediate action on important meetings.
169+
141170
## API Reference
142171

143172
The Grain API uses the following structure:

grain/app.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
},
88
"description": "Grain App Connection - Access and manage your meeting recordings",
99
"icon": "https://grain.com/favicon.ico",
10-
"unlisted": false
10+
"unlisted": false,
11+
"bindings": [
12+
{
13+
"type": "mcp",
14+
"name": "DATABASE",
15+
"app_name": "@deco/postgres"
16+
}
17+
]
1118
}
1219

20+
21+
22+

grain/server/constants.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
/**
2-
* Grain API constants and configuration
32
* API Documentation: https://developers.grain.com/
4-
*
5-
* IMPORTANT: Grain API uses GET with query parameters!
6-
* Correct endpoint: /_/public-api/recordings (ONE underscore, no /v2)
73
*/
8-
94
export const GRAIN_BASE_URL = "https://api.grain.com";
10-
export const GRAIN_LIST_RECORDINGS_ENDPOINT = "/_/public-api/recordings"; // GET method!
11-
export const GRAIN_RECORDING_ENDPOINT = "/_/public-api/recordings"; // GET method
5+
export const GRAIN_LIST_RECORDINGS_ENDPOINT = "/_/public-api/recordings";
6+
export const GRAIN_RECORDING_ENDPOINT = "/_/public-api/recordings";
127
export const GRAIN_TRANSCRIPT_ENDPOINT =
138
"/_/public-api/recordings/:id/transcript";
149

15-
// Default pagination
10+
// Webhook endpoints (API v2)
11+
export const GRAIN_CREATE_WEBHOOK_ENDPOINT = "/_/public-api/v2/hooks/create";
12+
export const GRAIN_LIST_WEBHOOKS_ENDPOINT = "/_/public-api/v2/hooks";
13+
export const GRAIN_DELETE_WEBHOOK_ENDPOINT = "/_/public-api/v2/hooks";
14+
15+
export const GRAIN_API_VERSION = "2025-10-31";
16+
1617
export const DEFAULT_PAGE_SIZE = 50;
1718
export const MAX_PAGE_SIZE = 100;

grain/server/lib/grain-client.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ import {
77
GRAIN_BASE_URL,
88
GRAIN_LIST_RECORDINGS_ENDPOINT,
99
GRAIN_RECORDING_ENDPOINT,
10+
GRAIN_CREATE_WEBHOOK_ENDPOINT,
11+
GRAIN_LIST_WEBHOOKS_ENDPOINT,
12+
GRAIN_DELETE_WEBHOOK_ENDPOINT,
13+
GRAIN_API_VERSION,
1014
} from "../constants.ts";
1115
import type {
1216
ListRecordingsParams,
1317
ListRecordingsResponse,
14-
Recording,
1518
RecordingDetails,
19+
WebhookConfig,
20+
CreateWebhookResponse,
21+
ListWebhooksResponse,
1622
} from "./types.ts";
1723

1824
export interface GrainClientConfig {
@@ -30,13 +36,20 @@ export class GrainClient {
3036

3137
/**
3238
* Build headers for API requests
39+
* Note: API v2 requires the Public-Api-Version header
3340
*/
34-
private getHeaders(): Record<string, string> {
35-
return {
41+
private getHeaders(includeApiVersion = false): Record<string, string> {
42+
const headers: Record<string, string> = {
3643
Authorization: `Bearer ${this.apiKey}`,
3744
"Content-Type": "application/json",
3845
Accept: "application/json",
3946
};
47+
48+
if (includeApiVersion) {
49+
headers["Public-Api-Version"] = GRAIN_API_VERSION;
50+
}
51+
52+
return headers;
4053
}
4154

4255
/**
@@ -147,4 +160,74 @@ export class GrainClient {
147160
search: query,
148161
});
149162
}
163+
164+
/**
165+
* Create a webhook to receive real-time notifications from Grain
166+
* API v2 endpoint: POST /_/public-api/v2/hooks/create
167+
*
168+
* Note: Grain performs a reachability test on the provided URL.
169+
* The endpoint MUST respond with a 2xx status for the hook to be created.
170+
*
171+
* @param config - Webhook configuration with hook_url and optional filter/include
172+
* @returns Created webhook details
173+
*/
174+
async createWebhook(config: WebhookConfig): Promise<CreateWebhookResponse> {
175+
const response = await fetch(
176+
`${this.baseUrl}${GRAIN_CREATE_WEBHOOK_ENDPOINT}`,
177+
{
178+
method: "POST",
179+
headers: this.getHeaders(true),
180+
body: JSON.stringify(config),
181+
},
182+
);
183+
184+
if (!response.ok) {
185+
const errorText = await response.text().catch(() => "Unknown error");
186+
throw new Error(`Grain API error (${response.status}): ${errorText}`);
187+
}
188+
189+
return (await response.json()) as CreateWebhookResponse;
190+
}
191+
192+
/**
193+
* List all webhooks configured for this account
194+
* API v2 endpoint: POST /_/public-api/v2/hooks (yes, it's POST not GET!)
195+
* @returns List of webhooks
196+
*/
197+
async listWebhooks(): Promise<ListWebhooksResponse> {
198+
const response = await fetch(
199+
`${this.baseUrl}${GRAIN_LIST_WEBHOOKS_ENDPOINT}`,
200+
{
201+
method: "POST", // Grain API v2 uses POST for listing hooks
202+
headers: this.getHeaders(true),
203+
},
204+
);
205+
206+
if (!response.ok) {
207+
const errorText = await response.text().catch(() => "Unknown error");
208+
throw new Error(`Grain API error (${response.status}): ${errorText}`);
209+
}
210+
211+
return (await response.json()) as ListWebhooksResponse;
212+
}
213+
214+
/**
215+
* Delete a webhook by its ID
216+
* API v2 endpoint: DELETE /_/public-api/v2/hooks/{id}
217+
* @param webhookId - The ID of the webhook to delete
218+
*/
219+
async deleteWebhook(webhookId: string): Promise<void> {
220+
const response = await fetch(
221+
`${this.baseUrl}${GRAIN_DELETE_WEBHOOK_ENDPOINT}/${webhookId}`,
222+
{
223+
method: "DELETE",
224+
headers: this.getHeaders(true),
225+
},
226+
);
227+
228+
if (!response.ok) {
229+
const errorText = await response.text().catch(() => "Unknown error");
230+
throw new Error(`Grain API error (${response.status}): ${errorText}`);
231+
}
232+
}
150233
}

0 commit comments

Comments
 (0)