-
Notifications
You must be signed in to change notification settings - Fork 5.8k
grain: AI-optimize MCP action set #21958
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
d8265a8
159052e
cf4d511
3297f08
a2a7f5f
686f515
20997ee
a2b9635
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import fs from "fs"; | ||
| import grain from "../../grain.app.mjs"; | ||
|
|
||
| const EXTENSION_BY_CONTENT_TYPE = { | ||
| "video/mp4": "mp4", | ||
| "video/quicktime": "mov", | ||
| "audio/mpeg": "mp3", | ||
| "audio/mp4": "m4a", | ||
| }; | ||
| const DEFAULT_EXTENSION = "mp4"; | ||
|
|
||
| export default { | ||
| key: "grain-download-recording", | ||
| name: "Download Recording", | ||
| description: "Downloads a Grain recording's media file (video or audio) and writes it to File Stash, returning `filePath`, `filename`, and `contentType`." | ||
| + " Only recordings with processed media have a downloadable file — check `media_type` (`video` or `audio`, not `transcript`) from **List Recordings** or **Get Recording** first." | ||
| + " Use **Get Transcript** instead if you only need the spoken content, not the media file itself." | ||
| + " Example: `recordingId: \"8a089fcb-0961-4393-8da2-f0db5f8cfd79\"` downloads the file and returns `{\"filePath\": \"/tmp/grain-recording-8a089fcb-....mp4\", \"filename\": \"grain-recording-8a089fcb-....mp4\", \"contentType\": \"video/mp4\"}`." | ||
| + " [See the documentation](https://developers.grain.com/#download-recording)", | ||
| version: "0.0.1", | ||
|
michelle0927 marked this conversation as resolved.
|
||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: true, | ||
| }, | ||
| type: "action", | ||
| props: { | ||
| grain, | ||
| recordingId: { | ||
| propDefinition: [ | ||
| grain, | ||
| "recordingId", | ||
| ], | ||
| }, | ||
| syncDir: { | ||
| type: "dir", | ||
| accessMode: "write", | ||
| sync: true, | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const response = await this.grain.downloadRecording({ | ||
| $, | ||
| recordingId: this.recordingId, | ||
| responseType: "arraybuffer", | ||
| returnFullResponse: true, | ||
| }); | ||
|
|
||
| const contentType = response.headers["content-type"]; | ||
| const extension = EXTENSION_BY_CONTENT_TYPE[contentType] ?? DEFAULT_EXTENSION; | ||
| const filename = `grain-recording-${this.recordingId}.${extension}`; | ||
| const filePath = `${process.env.STASH_DIR || "/tmp"}/${filename}`; | ||
|
|
||
| fs.writeFileSync(filePath, Buffer.from(response.data)); | ||
|
|
||
| $.export("$summary", `Downloaded recording ${this.recordingId} (${filename})`); | ||
| return { | ||
| filePath, | ||
| filename, | ||
| contentType, | ||
| }; | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import grain from "../../grain.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "grain-list-meeting-types", | ||
| name: "List Meeting Types", | ||
| description: "Lists the meeting types configured in your Grain workspace (id, name, scope), where scope is `internal` or `external`." | ||
| + " Use this to understand how recordings are categorized, or to resolve a meeting type's ID for filtering." | ||
| + " Example: returns `[{\"id\": \"e8b894a9-7ecf-4330-a282-527c085618fd\", \"name\": \"Sales\", \"scope\": \"external\"}, {\"id\": \"815747d1-9e25-40f6-a3b8-e1908e25151e\", \"name\": \"1:1s\", \"scope\": \"internal\"}]`." | ||
| + " [See the documentation](https://developers.grain.com/#list-meeting-types)", | ||
| version: "0.0.1", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: true, | ||
| }, | ||
| type: "action", | ||
| props: { | ||
| grain, | ||
| }, | ||
| async run({ $ }) { | ||
| const { meeting_types: meetingTypes } = await this.grain.listMeetingTypes({ | ||
| $, | ||
| }); | ||
|
|
||
| $.export("$summary", `Found ${meetingTypes.length} meeting type${meetingTypes.length === 1 | ||
| ? "" | ||
| : "s"}`); | ||
| return meetingTypes; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,18 @@ | ||||||||||||||||||||||||||||||||||||||
| import { ConfigurationError } from "@pipedream/platform"; | ||||||||||||||||||||||||||||||||||||||
| import grain from "../../grain.app.mjs"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export default { | ||||||||||||||||||||||||||||||||||||||
| key: "grain-list-recordings", | ||||||||||||||||||||||||||||||||||||||
| name: "List Recordings", | ||||||||||||||||||||||||||||||||||||||
| description: "Lists Grain recordings, optionally filtered by start datetime range (ISO8601), title search, or participant scope." | ||||||||||||||||||||||||||||||||||||||
| description: "Lists Grain recordings, optionally filtered by start datetime range (ISO8601), title search, participant scope, team, or meeting type." | ||||||||||||||||||||||||||||||||||||||
| + " Automatically paginates and returns up to Max Results recordings." | ||||||||||||||||||||||||||||||||||||||
| + " Use this to find recording IDs for **Get Recording** and **Get Transcript**." | ||||||||||||||||||||||||||||||||||||||
| + " Example: `titleSearch: \"Acme\"` returns recordings like" | ||||||||||||||||||||||||||||||||||||||
| + " `[{\"id\": \"pppp6666-qq77-rr88-ss99-tttt00000000\", \"title\": \"Acme Renewal Call\", \"start_datetime\": \"2026-01-05T15:00:00Z\", \"media_type\": \"video\", ...}]`." | ||||||||||||||||||||||||||||||||||||||
| + " Pass `fields` to return only the fields you need instead of the full object." | ||||||||||||||||||||||||||||||||||||||
| + " [See the documentation](https://developers.grain.com/#list-recordings)", | ||||||||||||||||||||||||||||||||||||||
| version: "0.0.1", | ||||||||||||||||||||||||||||||||||||||
| version: "0.1.0", | ||||||||||||||||||||||||||||||||||||||
| ai: "optimized", | ||||||||||||||||||||||||||||||||||||||
| annotations: { | ||||||||||||||||||||||||||||||||||||||
| destructiveHint: false, | ||||||||||||||||||||||||||||||||||||||
| openWorldHint: true, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -19,13 +24,15 @@ export default { | |||||||||||||||||||||||||||||||||||||
| beforeDatetime: { | ||||||||||||||||||||||||||||||||||||||
| type: "string", | ||||||||||||||||||||||||||||||||||||||
| label: "Before Datetime", | ||||||||||||||||||||||||||||||||||||||
| description: "Only return recordings that started before this ISO8601 datetime. E.g. `2025-01-01T00:00:00Z`", | ||||||||||||||||||||||||||||||||||||||
| description: "Only return recordings that started before this ISO8601 datetime. E.g. `2025-01-01T00:00:00Z`." | ||||||||||||||||||||||||||||||||||||||
| + " Verified against the live API to filter at day granularity — a cutoff earlier or later than a recording's calendar day works reliably, but a same-day cutoff may not exclude recordings from later that same day.", | ||||||||||||||||||||||||||||||||||||||
| optional: true, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| afterDatetime: { | ||||||||||||||||||||||||||||||||||||||
| type: "string", | ||||||||||||||||||||||||||||||||||||||
| label: "After Datetime", | ||||||||||||||||||||||||||||||||||||||
| description: "Only return recordings that started after this ISO8601 datetime. E.g. `2025-01-01T00:00:00Z`", | ||||||||||||||||||||||||||||||||||||||
| description: "Only return recordings that started after this ISO8601 datetime. E.g. `2025-01-01T00:00:00Z`." | ||||||||||||||||||||||||||||||||||||||
| + " Verified against the live API to filter at day granularity — a cutoff earlier or later than a recording's calendar day works reliably, but a same-day cutoff may not exclude recordings from earlier that same day.", | ||||||||||||||||||||||||||||||||||||||
| optional: true, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| titleSearch: { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -44,13 +51,33 @@ export default { | |||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||
| optional: true, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| team: { | ||||||||||||||||||||||||||||||||||||||
| type: "string", | ||||||||||||||||||||||||||||||||||||||
| label: "Team ID", | ||||||||||||||||||||||||||||||||||||||
| description: "Only return recordings belonging to this team. Use **List Teams** to find team IDs.", | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||
| optional: true, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| meetingType: { | ||||||||||||||||||||||||||||||||||||||
| type: "string", | ||||||||||||||||||||||||||||||||||||||
| label: "Meeting Type ID", | ||||||||||||||||||||||||||||||||||||||
| description: "Only return recordings with this meeting type. Use **List Meeting Types** to find meeting type IDs.", | ||||||||||||||||||||||||||||||||||||||
| optional: true, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| maxResults: { | ||||||||||||||||||||||||||||||||||||||
| type: "integer", | ||||||||||||||||||||||||||||||||||||||
| label: "Max Results", | ||||||||||||||||||||||||||||||||||||||
| description: "Maximum number of recordings to return. Must be a positive integer.", | ||||||||||||||||||||||||||||||||||||||
| optional: true, | ||||||||||||||||||||||||||||||||||||||
| default: 100, | ||||||||||||||||||||||||||||||||||||||
| min: 1, | ||||||||||||||||||||||||||||||||||||||
| max: 500, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| fields: { | ||||||||||||||||||||||||||||||||||||||
| type: "string[]", | ||||||||||||||||||||||||||||||||||||||
| label: "Fields", | ||||||||||||||||||||||||||||||||||||||
| description: "Only include these fields in each returned recording (e.g. `[\"id\", \"title\", \"start_datetime\"]`)." | ||||||||||||||||||||||||||||||||||||||
| + " Omit fields to return the full recording object for each result.", | ||||||||||||||||||||||||||||||||||||||
| optional: true, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| async run({ $ }) { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -59,6 +86,8 @@ export default { | |||||||||||||||||||||||||||||||||||||
| after_datetime: this.afterDatetime, | ||||||||||||||||||||||||||||||||||||||
| title_search: this.titleSearch, | ||||||||||||||||||||||||||||||||||||||
| participant_scope: this.participantScope, | ||||||||||||||||||||||||||||||||||||||
| team: this.team, | ||||||||||||||||||||||||||||||||||||||
| meeting_type: this.meetingType, | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const recordings = []; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -84,6 +113,22 @@ export default { | |||||||||||||||||||||||||||||||||||||
| $.export("$summary", `Successfully fetched ${recordings.length} recording${recordings.length === 1 | ||||||||||||||||||||||||||||||||||||||
| ? "" | ||||||||||||||||||||||||||||||||||||||
| : "s"}`); | ||||||||||||||||||||||||||||||||||||||
| return recordings; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const fields = typeof this.fields === "string" | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Validate fields before the first API request. The action fetches up to 500 recordings before it parses fields. Invalid Catch JSON.parse() errors. Also require every array element to be a Then remove the existing post-request parsing and validation. As per path instructions, “ConfigurationError is appropriate only for |
||||||||||||||||||||||||||||||||||||||
| ? JSON.parse(this.fields) | ||||||||||||||||||||||||||||||||||||||
| : this.fields; | ||||||||||||||||||||||||||||||||||||||
| if (fields !== undefined && !Array.isArray(fields)) { | ||||||||||||||||||||||||||||||||||||||
| throw new ConfigurationError("`fields` must be an array of field names."); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!fields?.length) { | ||||||||||||||||||||||||||||||||||||||
| return recordings; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return recordings.map((recording) => Object.fromEntries( | ||||||||||||||||||||||||||||||||||||||
| fields.map((field) => [ | ||||||||||||||||||||||||||||||||||||||
| field, | ||||||||||||||||||||||||||||||||||||||
| recording[field], | ||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import grain from "../../grain.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "grain-list-teams", | ||
| name: "List Teams", | ||
| description: "Lists the teams in your Grain workspace (id, name)." | ||
| + " Use this to resolve a team's ID before sharing a recording with it via **Manage Recording Sharing**." | ||
| + " Example: returns `[{\"id\": \"a414c333-c9fe-4fdc-9131-fb31796699b2\", \"name\": \"Pipedream\"}]`." | ||
| + " [See the documentation](https://developers.grain.com/#list-teams)", | ||
| version: "0.0.1", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: true, | ||
| }, | ||
| type: "action", | ||
| props: { | ||
| grain, | ||
| }, | ||
| async run({ $ }) { | ||
| const { teams } = await this.grain.listTeams({ | ||
| $, | ||
| }); | ||
|
|
||
| $.export("$summary", `Found ${teams.length} team${teams.length === 1 | ||
| ? "" | ||
| : "s"}`); | ||
| return teams; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import grain from "../../grain.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "grain-list-users", | ||
| name: "List Users", | ||
| description: "Lists the users in your Grain workspace (id, name, email)." | ||
| + " Use this to resolve a user's ID before sharing a recording with them via **Manage Recording Sharing**, or to identify who's who when reviewing recording participants." | ||
| + " Example: returns `[{\"id\": \"d91b7ed0-a149-425c-9623-0664148e4fc1\", \"name\": \"Danny Archer\", \"email\": \"darcher@pipedream.com\"}]`." | ||
| + " [See the documentation](https://developers.grain.com/#list-users)", | ||
| version: "0.0.1", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: true, | ||
| }, | ||
| type: "action", | ||
| props: { | ||
| grain, | ||
| }, | ||
| async run({ $ }) { | ||
| const { users } = await this.grain.listUsers({ | ||
| $, | ||
| }); | ||
|
|
||
| $.export("$summary", `Found ${users.length} user${users.length === 1 | ||
| ? "" | ||
| : "s"}`); | ||
| return users; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import grain from "../../grain.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "grain-manage-recording-sharing", | ||
| name: "Manage Recording Sharing", | ||
| description: "Shares or unshares a recording with a specific user or team." | ||
| + " Use **List Recordings** to find the recording's ID, and **List Users** or **List Teams** to resolve the target's ID." | ||
| + " Set `operation` to `share` to grant access or `unshare` to revoke it, and `targetType` to `user` or `team`." | ||
| + " Example: `recordingId: \"pppp6666-qq77-rr88-ss99-tttt00000000\", operation: \"share\", targetType: \"user\", targetId: \"d91b7ed0-a149-425c-9623-0664148e4fc1\"` shares the recording with that user and returns `{\"success\": true}`." | ||
| + " [See the documentation](https://developers.grain.com/#share-recording-to-a-team)", | ||
| version: "0.0.1", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: false, | ||
| }, | ||
| type: "action", | ||
| props: { | ||
| grain, | ||
| recordingId: { | ||
| propDefinition: [ | ||
| grain, | ||
| "recordingId", | ||
| ], | ||
| }, | ||
| operation: { | ||
| type: "string", | ||
| label: "Operation", | ||
| description: "Whether to share or unshare the recording.", | ||
| options: [ | ||
| "share", | ||
| "unshare", | ||
| ], | ||
| }, | ||
| targetType: { | ||
| type: "string", | ||
| label: "Target Type", | ||
| description: "Whether the target is a user or a team.", | ||
| options: [ | ||
| "user", | ||
| "team", | ||
| ], | ||
| }, | ||
| targetId: { | ||
| type: "string", | ||
| label: "Target ID", | ||
| description: "The ID of the user (from **List Users**) or team (from **List Teams**) to share or unshare the recording with. E.g. `d91b7ed0-a149-425c-9623-0664148e4fc1`.", | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| if (this.operation === "share") { | ||
| await this.grain.shareRecording({ | ||
| $, | ||
| recordingId: this.recordingId, | ||
| targetType: this.targetType, | ||
| data: { | ||
| [`${this.targetType}_id`]: this.targetId, | ||
| }, | ||
| }); | ||
| } else { | ||
| await this.grain.unshareRecording({ | ||
| $, | ||
| recordingId: this.recordingId, | ||
| targetType: this.targetType, | ||
| targetId: this.targetId, | ||
| }); | ||
| } | ||
|
|
||
| const summary = `${this.operation === "share" | ||
| ? "Shared" | ||
| : "Unshared"} recording ${this.recordingId} ${this.operation === "share" | ||
| ? "with" | ||
| : "from"} ${this.targetType} ${this.targetId}`; | ||
| $.export("$summary", summary); | ||
| return { | ||
| recordingId: this.recordingId, | ||
| operation: this.operation, | ||
| targetType: this.targetType, | ||
| targetId: this.targetId, | ||
| }; | ||
| }, | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.