From caf3210b13f608637eb7a5d9fad5160b6e4358c6 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Wed, 19 Nov 2025 15:36:14 +0530 Subject: [PATCH 01/21] add workspace access guidelines to instructions --- apps/cli/src/application/assistant/instructions.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/cli/src/application/assistant/instructions.ts b/apps/cli/src/application/assistant/instructions.ts index 3fd32af6..aca6a8a5 100644 --- a/apps/cli/src/application/assistant/instructions.ts +++ b/apps/cli/src/application/assistant/instructions.ts @@ -27,6 +27,11 @@ Always consult this catalog first so you load the right skills before taking act - Use relative paths (no \${BASE_DIR} prefixes) when running commands or referencing files. - Keep user data safe—double-check before editing or deleting important resources. +## Workspace access & scope +- You have full read/write access inside \`${BASE_DIR}\` (this resolves to the user's \`~/.rowboat\` directory). Create folders, files, and agents there using builtin tools or allowed shell commands—don't wait for the user to do it manually. +- If a user mentions a different root (e.g., \`~/.rowboatx\` or another path), clarify whether they meant the Rowboat workspace and propose the equivalent path you can act on. Only refuse if they explicitly insist on an inaccessible location. +- Prefer builtin file tools (\`createFile\`, \`updateFile\`, \`deleteFile\`, \`exploreDirectory\`) for workspace changes. Reserve refusal or "you do it" responses for cases that are truly outside the Rowboat sandbox. + ## Builtin Tools vs Shell Commands **IMPORTANT**: Rowboat provides builtin tools that are internal and do NOT require security allowlist entries: From ee03c861f66ce84a1b425c3f88094fdf0e92299a Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Wed, 19 Nov 2025 17:52:54 +0530 Subject: [PATCH 02/21] updated example --- apps/cli/examples/twitter-podcast.txt | 512 ++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 apps/cli/examples/twitter-podcast.txt diff --git a/apps/cli/examples/twitter-podcast.txt b/apps/cli/examples/twitter-podcast.txt new file mode 100644 index 00000000..8d05aa28 --- /dev/null +++ b/apps/cli/examples/twitter-podcast.txt @@ -0,0 +1,512 @@ +{ + "name": "tweet-podcast", + "description": "An agent that will produce a podcast from recent tweets", + "model": "gpt-5.1", + "instructions": "You are the orchestrator for producing a short podcast episode end-to-end. Follow these steps in order and only advance once each step succeeds:\n\n1. Tweets: call the tweets workflow to collect the latest tweets, .\n\n2.Transcript creation: Provide the resulting tweets to the podcast_transcript_agent tool so it can script a ~1 minute alternating dialogue between John and Chloe that references the tweets and a balanced conversation about AI bubble.\n\n4. Audio production: Send the transcript to the elevenlabs_audio_gen tool create an audio file.", + "tools": { + "tweets": { + "type": "agent", + "name": "tweets" + }, + "podcast_transcript_agent": { + "type": "agent", + "name": "podcast_transcript_agent" + }, + "elevenlabs_audio_gen": { + "type": "agent", + "name": "elevenlabs_audio_gen" + } + } +} + +{ + "name": "tweets", + "description": "Checks latest tweets", + "model": "gpt-4.1", + "instructions": "Pulls the recent 10 recent tweets each on OpenAI, Anthropic, Nvidia, Grok, Gemini", + "tools": { + "search_tweets": { + "type": "mcp", + "name": "TWITTER_RECENT_SEARCH", + "description": "Search recent Tweets from the last 7 days using X/Twitter's search syntax via Composio's Twitter MCP server.", + "mcpServerName": "twitter", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query for matching Tweets. Use X search operators like from:username, -is:retweet, -is:reply, has:media, lang:en, etc. Limited to last 7 days." + }, + "start_time": { + "type": "string", + "description": "Oldest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results, within the last 7 days." + }, + "end_time": { + "type": "string", + "description": "Newest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results; exclusive." + }, + "max_results": { + "type": "integer", + "description": "Number of Tweets to return (up to 2000 per call).", + "default": 10 + }, + "sort_order": { + "type": "string", + "enum": ["recency", "relevancy"], + "description": "Order of results: 'recency' (most recent first) or 'relevancy'." + }, + "tweet_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "article", + "attachments", + "author_id", + "card_uri", + "context_annotations", + "conversation_id", + "created_at", + "edit_controls", + "edit_history_tweet_ids", + "entities", + "geo", + "id", + "in_reply_to_user_id", + "lang", + "non_public_metrics", + "note_tweet", + "organic_metrics", + "possibly_sensitive", + "promoted_metrics", + "public_metrics", + "referenced_tweets", + "reply_settings", + "scopes", + "source", + "text", + "withheld" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tweet fields to include in the response. Example: ['created_at','author_id','public_metrics']." + }, + "expansions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "article.cover_media", + "article.media_entities", + "attachments.media_keys", + "attachments.media_source_tweet", + "attachments.poll_ids", + "author_id", + "author_screen_name", + "edit_history_tweet_ids", + "entities.mentions.username", + "entities.note.mentions.username", + "geo.place_id", + "in_reply_to_user_id", + "referenced_tweets.id", + "referenced_tweets.id.author_id" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Expansions to hydrate related objects like users, media, polls, and places." + }, + "media_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "alt_text", + "duration_ms", + "height", + "media_key", + "non_public_metrics", + "organic_metrics", + "preview_image_url", + "promoted_metrics", + "public_metrics", + "type", + "url", + "variants", + "width" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Media fields to include when media keys are expanded." + }, + "place_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "contained_within", + "country", + "country_code", + "full_name", + "geo", + "id", + "name", + "place_type" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Place fields to include when place IDs are expanded." + }, + "poll_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "duration_minutes", + "end_datetime", + "id", + "options", + "voting_status" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Poll fields to include when poll IDs are expanded." + }, + "user_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "affiliation", + "connection_status", + "created_at", + "description", + "entities", + "id", + "location", + "most_recent_tweet_id", + "name", + "pinned_tweet_id", + "profile_banner_url", + "profile_image_url", + "protected", + "public_metrics", + "receives_your_dm", + "subscription_type", + "url", + "verified", + "verified_type", + "withheld", + "username" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "User fields to include when user IDs are expanded. Username is always returned by default." + }, + "since_id": { + "type": "string", + "description": "Return Tweets more recent than this ID (cannot be used with start_time)." + }, + "until_id": { + "type": "string", + "description": "Return Tweets older than this ID (cannot be used with end_time)." + }, + "next_token": { + "type": "string", + "description": "Pagination token from a previous response's meta.next_token." + }, + "pagination_token": { + "type": "string", + "description": "Alternative pagination token from a previous meta.next_token; next_token is preferred." + } + }, + "required": ["query"], + "additionalProperties": false + } + }, + "bash": { + "type": "builtin", + "name": "executeCommand", + "description": "Execute bash commands to manipulate files like tweets.txt, e.g. writing search results to disk or appending logs.", + "inputSchema": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to execute, such as 'echo \"text\" >> tweets.txt' or 'cat tweets.txt'." + } + }, + "required": ["command"], + "additionalProperties": false + } + } + } + } + +{ + "name": "podcast_transcript_agent", + "description": "An agent that will generate a transcript of a podcast", + "model": "gpt-4.1", + "instructions": "You job is to create a NotebookLM style 1 minute podcast between 2 speakers John and Chloe. Each line should be a new speaker. The podcast should be about the contents of the two papers (that were selected). You can use [sighs], [inhales then exhales], [chuckles], [laughs], [clears throat], [coughs], [sniffs], [pauses] etc. to make the podcast more natural." +} + +{ + "name": "elevenlabs_audio_gen", + "description": "An agent that will generate an audio file from a text", + "model": "gpt-4.1", + "instructions": "Your job is to take the mutli speaker transcript and generate an audio file from it. Use the elevenlabs text to speech tool to do this. For each speaker turn, you should generate an audio file and then combine them all into a single audio file. Use the voice_name 'Liam' for John and 'Cassidy' for Chloe. Make sure to remove the speaker names from the text before generating the audio files. Use the eleven_v3 model_id. In addition, you should use the compose_music tool to generate a short musical intro and outro for the podcast. The intro should be a small 5-10 second clip modeled after popular podcasts which fades and the podcast starts. The outro should be 10-15 seconds of a related sound. Save the intro and outro to files, and then use the bash tool to stitch them with the main podcast audio so that the final output audio file starts with the intro music, then the full conversation, and ends with the outro music. Place all generated audio on the Desktop by default unless otherwise instructed. Don't wait for confirmation - go ahead and produce the podcast.", + "tools": { + "text_to_speech": { + "type": "mcp", + "name": "text_to_speech", + "description": "Generate an audio file from a text", + "mcpServerName": "elevenLabs", + "inputSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The text to generate an audio file from" + }, + "voice_name": { + "type": "string", + "description": "The voice name to use for the audio file" + }, + "model_id": { + "type": "string", + "description": "The model id to use for the audio file" + } + } + } + }, + "compose_music": { + "type": "mcp", + "name": "compose_music", + "description": "Generate intro and outro music for the podcast and save as audio files", + "mcpServerName": "elevenLabs", + "inputSchema": { + "type": "object", + "properties": { + "prompt": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ], + "default": null, + "title": "Prompt" + }, + "output_directory": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ], + "default": null, + "title": "Output Directory" + }, + "composition_plan": { + "anyOf": [ + {"$ref": "#/$defs/MusicPrompt"}, + {"type": "null"} + ], + "default": null + }, + "music_length_ms": { + "anyOf": [ + {"type": "integer"}, + {"type": "null"} + ], + "default": null, + "title": "Music Length Ms" + } + }, + "$defs": { + "MusicPrompt": { + "additionalProperties": true, + "properties": { + "positive_global_styles": { + "items": {"type": "string"}, + "title": "Positive Global Styles", + "type": "array" + }, + "negative_global_styles": { + "items": {"type": "string"}, + "title": "Negative Global Styles", + "type": "array" + }, + "sections": { + "items": {"$ref": "#/$defs/SongSection"}, + "title": "Sections", + "type": "array" + } + }, + "required": [ + "positive_global_styles", + "negative_global_styles", + "sections" + ], + "title": "MusicPrompt", + "type": "object" + }, + "SectionSource": { + "additionalProperties": true, + "properties": { + "song_id": { + "title": "Song Id", + "type": "string" + }, + "range": {"$ref": "#/$defs/TimeRange"}, + "negative_ranges": { + "anyOf": [ + { + "items": {"$ref": "#/$defs/TimeRange"}, + "type": "array" + }, + {"type": "null"} + ], + "default": null, + "title": "Negative Ranges" + } + }, + "required": [ + "song_id", + "range" + ], + "title": "SectionSource", + "type": "object" + }, + "SongSection": { + "additionalProperties": true, + "properties": { + "section_name": { + "title": "Section Name", + "type": "string" + }, + "positive_local_styles": { + "items": {"type": "string"}, + "title": "Positive Local Styles", + "type": "array" + }, + "negative_local_styles": { + "items": {"type": "string"}, + "title": "Negative Local Styles", + "type": "array" + }, + "duration_ms": { + "title": "Duration Ms", + "type": "integer" + }, + "lines": { + "items": {"type": "string"}, + "title": "Lines", + "type": "array" + }, + "source_from": { + "anyOf": [ + {"$ref": "#/$defs/SectionSource"}, + {"type": "null"} + ], + "default": null + } + }, + "required": [ + "section_name", + "positive_local_styles", + "negative_local_styles", + "duration_ms", + "lines" + ], + "title": "SongSection", + "type": "object" + }, + "TimeRange": { + "additionalProperties": true, + "properties": { + "start_ms": { + "title": "Start Ms", + "type": "integer" + }, + "end_ms": { + "title": "End Ms", + "type": "integer" + } + }, + "required": [ + "start_ms", + "end_ms" + ], + "title": "TimeRange", + "type": "object" + } + }, + "title": "compose_musicArguments" + } + }, + "bash": { + "type": "builtin", + "name": "executeCommand" + } + } +} + +{ + "mcpServers": { + "elevenLabs": { + "command": "uvx", + "args": [ + "elevenlabs-mcp" + ], + "env": { + "ELEVENLABS_API_KEY": "" + } + }, + "calendar": { + "type": "http", + "url": "" + }, + "twitter": { + "type": "http", + "url": "" + }, + } +} \ No newline at end of file From da9477b9097755834b89f24a4a93bcdc30123e0f Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Wed, 19 Nov 2025 17:54:09 +0530 Subject: [PATCH 03/21] removed incorrect example --- apps/cli/examples/notebooklm-podcast.json | 128 ---------------------- 1 file changed, 128 deletions(-) delete mode 100644 apps/cli/examples/notebooklm-podcast.json diff --git a/apps/cli/examples/notebooklm-podcast.json b/apps/cli/examples/notebooklm-podcast.json deleted file mode 100644 index 616546f2..00000000 --- a/apps/cli/examples/notebooklm-podcast.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "name": "podcast", - "description": "A workflow to create a podcast", - "steps": [ - { - "type": "agent", - "id": "arxiv-feed-reader" - }, - { - "type": "agent", - "id": "summarise-a-few" - }, - { - "type": "agent", - "id": "podcast_transcript_agent" - }, - { - "type": "agent", - "id": "elevenlabs_audio_gen" - } - ] -} - -{ - "name": "summariser_workflow", - "description": "A workflow to summarise an arxiv paper", - "steps": [ - { - "type": "agent", - "id": "summariser_agent" - } - ] -} - -{ - "name": "summariser_agent", - "description": "An agent that will summarise an arxiv paper", - "model": "gpt-4.1", - "instructions": "Your job is to download and summarise an arxiv paper. Use a command like this to do it\n\n curl -L -o paper.pdf https://arxiv.org/pdf/2511.02997 (use the url that the user provides you). Important, just put out the GIST of the paper in two lines. Dont ask a human for inputs - do what you think is best.", - "tools": { - "bash": { - "type": "builtin", - "name": "executeCommand" - } - } -} - -{ - "name": "arxiv-feed-reader", - "description": "A feed reader for the arXiv", - "model": "gpt-4.1", - "instructions": "Your job is to extract the latest papers from the arXiv feed and summarise them. Use an example curl command like the following to get this done:\n\n! curl -s https://rss.arxiv.org/rss/cs.AI \\\n| yq -p=xml -o=json \\\n| jq -r '.rss.channel.item[] | select(.title | test(\"agent\"; \"i\")) | \"\\(.title)\\n\\(.link)\\n\\(.description)\\n\"' \n\nThis will give you a list of papers that contain the word \"agent\" in the title. You can then summarise these papers using the summariser agent.", - "tools": { - "bash": { - "type": "builtin", - "name": "executeCommand" - } - } -} - -{ - "name": "summarise-a-few", - "description": "An agent that will summarise a few arxiv papers", - "model": "gpt-4.1", - "instructions": "Your job is to pick 2 interesting papers and related papers on the same topic, and then summarise each of them inidivually using the right tool calls. Make sure to pass in the URL of the paper to the summaurse tool. Don't ask for human input.", - "tools": { - "summariser": { - "type": "agent", - "name": "summariser_workflow" - } - } -} - -{ - "name": "podcast_transcript_agent", - "description": "An agent that will generate a transcript of a podcast", - "model": "gpt-4.1", - "instructions": "You job is to create a NotebookLM style 1 minute podcast between 2 speakers John and Chloe. Each line should be a new speaker. The podcast should be about the contents of the two papers (that were selected). You can use [sighs], [inhales then exhales], [chuckles], [laughs], [clears throat], [coughs], [sniffs], [pauses] etc. to make the podcast more natural." -} - -{ - "name": "elevenlabs_audio_gen", - "description": "An agent that will generate an audio file from a text", - "model": "gpt-4.1", - "instructions": "Your job is to take the mutli speaker transcript and generate an audio file from it. Use the elevenlabs text to speech tool to do this. For each speaker turn, you should generate an audio file and then combine them all into a single audio file. Use the voice_name 'Liam' for John and 'Cassidy' for Chloe. Make sure to remove the speaker names from the text before generating the audio files. Use the bash tool to look for the generated audio files and also combine the audio files into a single final podcast audio file. Use 'eleven_v3' for the model_id.", - "tools": { - "text_to_speech": { - "type": "mcp", - "name": "text_to_speech", - "description": "Generate an audio file from a text", - "mcpServerName": "elevenLabs", - "inputSchema": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "The text to generate an audio file from" - }, - "voice_name": { - "type": "string", - "description": "The voice name to use for the audio file" - }, - "model_id": { - "type": "string", - "description": "The model id to use for the audio file" - } - } - } - }, - "bash": { - "type": "builtin", - "name": "executeCommand" - } - } -} - -{ - "mcpServers": { - - "elevenLabs": { - "command": "uvx", - "args": ["elevenlabs-mcp"], - "env": { - "ELEVENLABS_API_KEY": "sk_42ee2a0a19266552c18b0920b593e22f0185d4b1435b65ed" - } - } - } -} \ No newline at end of file From cd8fd4fe00df49eb299e443a79df8ceae58255de Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 20 Nov 2025 17:12:33 +0530 Subject: [PATCH 04/21] add --example to add the examples from rowboat --- apps/cli/bin/app.js | 47 +- apps/cli/examples/twitter-podcast.json | 558 ++++++++++++++++++ apps/cli/examples/twitter-podcast.txt | 512 ---------------- apps/cli/package.json | 3 +- apps/cli/src/application/entities/mcp.ts | 14 +- .../application/examples/import-example.ts | 115 ++++ 6 files changed, 724 insertions(+), 525 deletions(-) create mode 100644 apps/cli/examples/twitter-podcast.json delete mode 100644 apps/cli/examples/twitter-podcast.txt create mode 100644 apps/cli/src/application/examples/import-example.ts diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index cdc12ed6..4ba5aa84 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -1,7 +1,8 @@ #!/usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { app } from '../dist/app.js'; +import { app, updateState } from '../dist/app.js'; +import { importExample, listAvailableExamples } from '../dist/application/examples/import-example.js'; yargs(hideBin(process.argv)) @@ -11,8 +12,7 @@ yargs(hideBin(process.argv)) (y) => y .option("agent", { type: "string", - description: "The agent to run", - default: "copilot", + description: "The agent to run (defaults to copilot)", }) .option("run_id", { type: "string", @@ -26,10 +26,43 @@ yargs(hideBin(process.argv)) type: "boolean", description: "Do not interact with the user", default: false, + }) + .option("example", { + type: "string", + description: "Import an example workflow by name (use 'all' for every example) before running", }), - (argv) => { - app({ - agent: argv.agent, + async (argv) => { + let agent = argv.agent ?? "copilot"; + if (argv.example) { + const requested = String(argv.example).trim(); + const isAll = requested.toLowerCase() === "all"; + try { + const examplesToImport = isAll ? await listAvailableExamples() : [requested]; + if (examplesToImport.length === 0) { + console.error("No packaged examples are available to import."); + process.exit(1); + } + for (const exampleName of examplesToImport) { + const imported = await importExample(exampleName); + const agentList = imported.importedAgents.join(", "); + console.error(`Imported example '${exampleName}' with agents: ${agentList}`); + console.error(`Primary agent: ${imported.entryAgent}`); + if (imported.addedServers.length > 0) { + console.error(`Configured new MCP servers: ${imported.addedServers.join(", ")}`); + } + if (imported.skippedServers.length > 0) { + console.error(`Skipped existing MCP servers (already configured): ${imported.skippedServers.join(", ")}`); + } + } + } catch (error) { + console.error(error?.message ?? error); + process.exit(1); + } + console.error("Examples imported. Re-run rowboatx without --example (or with --agent ) when you're ready to chat."); + return; + } + await app({ + agent, runId: argv.run_id, input: argv.input, noInteractive: argv.noInteractive, @@ -52,4 +85,4 @@ yargs(hideBin(process.argv)) updateState(argv.agent, argv.run_id); } ) - .parse(); \ No newline at end of file + .parse(); diff --git a/apps/cli/examples/twitter-podcast.json b/apps/cli/examples/twitter-podcast.json new file mode 100644 index 00000000..d008fcb6 --- /dev/null +++ b/apps/cli/examples/twitter-podcast.json @@ -0,0 +1,558 @@ +{ + "id": "twitter-podcast", + "description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.", + "entryAgent": "tweet-podcast", + "agents": [ + { + "name": "tweet-podcast", + "description": "An agent that will produce a podcast from recent tweets", + "model": "gpt-5.1", + "instructions": "You are the orchestrator for producing a short podcast episode end-to-end. Follow these steps in order and only advance once each step succeeds:\n\n1. Tweets: call the tweets workflow to collect the latest tweets, .\n\n2.Transcript creation: Provide the resulting tweets to the podcast_transcript_agent tool so it can script a ~1 minute alternating dialogue between John and Chloe that references the tweets and a balanced conversation about AI bubble.\n\n4. Audio production: Send the transcript to the elevenlabs_audio_gen tool create an audio file.", + "tools": { + "tweets": { + "type": "agent", + "name": "tweets" + }, + "podcast_transcript_agent": { + "type": "agent", + "name": "podcast_transcript_agent" + }, + "elevenlabs_audio_gen": { + "type": "agent", + "name": "elevenlabs_audio_gen" + } + } + }, + { + "name": "tweets", + "description": "Checks latest tweets", + "model": "gpt-4.1", + "instructions": "Pulls the recent 10 recent tweets each on OpenAI, Anthropic, Nvidia, Grok, Gemini", + "tools": { + "search_tweets": { + "type": "mcp", + "name": "TWITTER_RECENT_SEARCH", + "description": "Search recent Tweets from the last 7 days using X/Twitter's search syntax via Composio's Twitter MCP server.", + "mcpServerName": "twitter", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query for matching Tweets. Use X search operators like from:username, -is:retweet, -is:reply, has:media, lang:en, etc. Limited to last 7 days." + }, + "start_time": { + "type": "string", + "description": "Oldest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results, within the last 7 days." + }, + "end_time": { + "type": "string", + "description": "Newest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results; exclusive." + }, + "max_results": { + "type": "integer", + "description": "Number of Tweets to return (up to 2000 per call).", + "default": 10 + }, + "sort_order": { + "type": "string", + "enum": [ + "recency", + "relevancy" + ], + "description": "Order of results: 'recency' (most recent first) or 'relevancy'." + }, + "tweet_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "article", + "attachments", + "author_id", + "card_uri", + "context_annotations", + "conversation_id", + "created_at", + "edit_controls", + "edit_history_tweet_ids", + "entities", + "geo", + "id", + "in_reply_to_user_id", + "lang", + "non_public_metrics", + "note_tweet", + "organic_metrics", + "possibly_sensitive", + "promoted_metrics", + "public_metrics", + "referenced_tweets", + "reply_settings", + "scopes", + "source", + "text", + "withheld" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tweet fields to include in the response. Example: ['created_at','author_id','public_metrics']." + }, + "expansions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "article.cover_media", + "article.media_entities", + "attachments.media_keys", + "attachments.media_source_tweet", + "attachments.poll_ids", + "author_id", + "author_screen_name", + "edit_history_tweet_ids", + "entities.mentions.username", + "entities.note.mentions.username", + "geo.place_id", + "in_reply_to_user_id", + "referenced_tweets.id", + "referenced_tweets.id.author_id" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Expansions to hydrate related objects like users, media, polls, and places." + }, + "media_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "alt_text", + "duration_ms", + "height", + "media_key", + "non_public_metrics", + "organic_metrics", + "preview_image_url", + "promoted_metrics", + "public_metrics", + "type", + "url", + "variants", + "width" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Media fields to include when media keys are expanded." + }, + "place_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "contained_within", + "country", + "country_code", + "full_name", + "geo", + "id", + "name", + "place_type" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Place fields to include when place IDs are expanded." + }, + "poll_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "duration_minutes", + "end_datetime", + "id", + "options", + "voting_status" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Poll fields to include when poll IDs are expanded." + }, + "user_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "affiliation", + "connection_status", + "created_at", + "description", + "entities", + "id", + "location", + "most_recent_tweet_id", + "name", + "pinned_tweet_id", + "profile_banner_url", + "profile_image_url", + "protected", + "public_metrics", + "receives_your_dm", + "subscription_type", + "url", + "verified", + "verified_type", + "withheld", + "username" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "User fields to include when user IDs are expanded. Username is always returned by default." + }, + "since_id": { + "type": "string", + "description": "Return Tweets more recent than this ID (cannot be used with start_time)." + }, + "until_id": { + "type": "string", + "description": "Return Tweets older than this ID (cannot be used with end_time)." + }, + "next_token": { + "type": "string", + "description": "Pagination token from a previous response's meta.next_token." + }, + "pagination_token": { + "type": "string", + "description": "Alternative pagination token from a previous meta.next_token; next_token is preferred." + } + }, + "required": [ + "query" + ], + "additionalProperties": false + } + }, + "bash": { + "type": "builtin", + "name": "executeCommand", + "description": "Execute bash commands to manipulate files like tweets.txt, e.g. writing search results to disk or appending logs.", + "inputSchema": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to execute, such as 'echo \"text\" >> tweets.txt' or 'cat tweets.txt'." + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + } + } + }, + { + "name": "podcast_transcript_agent", + "description": "An agent that will generate a transcript of a podcast", + "model": "gpt-4.1", + "instructions": "You job is to create a NotebookLM style 1 minute podcast between 2 speakers John and Chloe. Each line should be a new speaker. The podcast should be about the contents of the two papers (that were selected). You can use [sighs], [inhales then exhales], [chuckles], [laughs], [clears throat], [coughs], [sniffs], [pauses] etc. to make the podcast more natural." + }, + { + "name": "elevenlabs_audio_gen", + "description": "An agent that will generate an audio file from a text", + "model": "gpt-4.1", + "instructions": "Your job is to take the mutli speaker transcript and generate an audio file from it. Use the elevenlabs text to speech tool to do this. For each speaker turn, you should generate an audio file and then combine them all into a single audio file. Use the voice_name 'Liam' for John and 'Cassidy' for Chloe. Make sure to remove the speaker names from the text before generating the audio files. Use the eleven_v3 model_id. In addition, you should use the compose_music tool to generate a short musical intro and outro for the podcast. The intro should be a small 5-10 second clip modeled after popular podcasts which fades and the podcast starts. The outro should be 10-15 seconds of a related sound. Save the intro and outro to files, and then use the bash tool to stitch them with the main podcast audio so that the final output audio file starts with the intro music, then the full conversation, and ends with the outro music. Place all generated audio on the Desktop by default unless otherwise instructed. Don't wait for confirmation - go ahead and produce the podcast.", + "tools": { + "text_to_speech": { + "type": "mcp", + "name": "text_to_speech", + "description": "Generate an audio file from a text", + "mcpServerName": "elevenLabs", + "inputSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The text to generate an audio file from" + }, + "voice_name": { + "type": "string", + "description": "The voice name to use for the audio file" + }, + "model_id": { + "type": "string", + "description": "The model id to use for the audio file" + } + } + } + }, + "compose_music": { + "type": "mcp", + "name": "compose_music", + "description": "Generate intro and outro music for the podcast and save as audio files", + "mcpServerName": "elevenLabs", + "inputSchema": { + "type": "object", + "properties": { + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Prompt" + }, + "output_directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Directory" + }, + "composition_plan": { + "anyOf": [ + { + "$ref": "#/$defs/MusicPrompt" + }, + { + "type": "null" + } + ], + "default": null + }, + "music_length_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Music Length Ms" + } + }, + "$defs": { + "MusicPrompt": { + "additionalProperties": true, + "properties": { + "positive_global_styles": { + "items": { + "type": "string" + }, + "title": "Positive Global Styles", + "type": "array" + }, + "negative_global_styles": { + "items": { + "type": "string" + }, + "title": "Negative Global Styles", + "type": "array" + }, + "sections": { + "items": { + "$ref": "#/$defs/SongSection" + }, + "title": "Sections", + "type": "array" + } + }, + "required": [ + "positive_global_styles", + "negative_global_styles", + "sections" + ], + "title": "MusicPrompt", + "type": "object" + }, + "SectionSource": { + "additionalProperties": true, + "properties": { + "song_id": { + "title": "Song Id", + "type": "string" + }, + "range": { + "$ref": "#/$defs/TimeRange" + }, + "negative_ranges": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TimeRange" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Negative Ranges" + } + }, + "required": [ + "song_id", + "range" + ], + "title": "SectionSource", + "type": "object" + }, + "SongSection": { + "additionalProperties": true, + "properties": { + "section_name": { + "title": "Section Name", + "type": "string" + }, + "positive_local_styles": { + "items": { + "type": "string" + }, + "title": "Positive Local Styles", + "type": "array" + }, + "negative_local_styles": { + "items": { + "type": "string" + }, + "title": "Negative Local Styles", + "type": "array" + }, + "duration_ms": { + "title": "Duration Ms", + "type": "integer" + }, + "lines": { + "items": { + "type": "string" + }, + "title": "Lines", + "type": "array" + }, + "source_from": { + "anyOf": [ + { + "$ref": "#/$defs/SectionSource" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "section_name", + "positive_local_styles", + "negative_local_styles", + "duration_ms", + "lines" + ], + "title": "SongSection", + "type": "object" + }, + "TimeRange": { + "additionalProperties": true, + "properties": { + "start_ms": { + "title": "Start Ms", + "type": "integer" + }, + "end_ms": { + "title": "End Ms", + "type": "integer" + } + }, + "required": [ + "start_ms", + "end_ms" + ], + "title": "TimeRange", + "type": "object" + } + }, + "title": "compose_musicArguments" + } + }, + "bash": { + "type": "builtin", + "name": "executeCommand" + } + } + } + ], + "mcpServers": { + "elevenLabs": { + "command": "uvx", + "args": [ + "elevenlabs-mcp" + ], + "env": { + "ELEVENLABS_API_KEY": "" + } + }, + "calendar": { + "type": "http", + "url": "" + }, + "twitter": { + "type": "http", + "url": "" + } + } +} diff --git a/apps/cli/examples/twitter-podcast.txt b/apps/cli/examples/twitter-podcast.txt deleted file mode 100644 index 8d05aa28..00000000 --- a/apps/cli/examples/twitter-podcast.txt +++ /dev/null @@ -1,512 +0,0 @@ -{ - "name": "tweet-podcast", - "description": "An agent that will produce a podcast from recent tweets", - "model": "gpt-5.1", - "instructions": "You are the orchestrator for producing a short podcast episode end-to-end. Follow these steps in order and only advance once each step succeeds:\n\n1. Tweets: call the tweets workflow to collect the latest tweets, .\n\n2.Transcript creation: Provide the resulting tweets to the podcast_transcript_agent tool so it can script a ~1 minute alternating dialogue between John and Chloe that references the tweets and a balanced conversation about AI bubble.\n\n4. Audio production: Send the transcript to the elevenlabs_audio_gen tool create an audio file.", - "tools": { - "tweets": { - "type": "agent", - "name": "tweets" - }, - "podcast_transcript_agent": { - "type": "agent", - "name": "podcast_transcript_agent" - }, - "elevenlabs_audio_gen": { - "type": "agent", - "name": "elevenlabs_audio_gen" - } - } -} - -{ - "name": "tweets", - "description": "Checks latest tweets", - "model": "gpt-4.1", - "instructions": "Pulls the recent 10 recent tweets each on OpenAI, Anthropic, Nvidia, Grok, Gemini", - "tools": { - "search_tweets": { - "type": "mcp", - "name": "TWITTER_RECENT_SEARCH", - "description": "Search recent Tweets from the last 7 days using X/Twitter's search syntax via Composio's Twitter MCP server.", - "mcpServerName": "twitter", - "inputSchema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query for matching Tweets. Use X search operators like from:username, -is:retweet, -is:reply, has:media, lang:en, etc. Limited to last 7 days." - }, - "start_time": { - "type": "string", - "description": "Oldest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results, within the last 7 days." - }, - "end_time": { - "type": "string", - "description": "Newest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results; exclusive." - }, - "max_results": { - "type": "integer", - "description": "Number of Tweets to return (up to 2000 per call).", - "default": 10 - }, - "sort_order": { - "type": "string", - "enum": ["recency", "relevancy"], - "description": "Order of results: 'recency' (most recent first) or 'relevancy'." - }, - "tweet_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "article", - "attachments", - "author_id", - "card_uri", - "context_annotations", - "conversation_id", - "created_at", - "edit_controls", - "edit_history_tweet_ids", - "entities", - "geo", - "id", - "in_reply_to_user_id", - "lang", - "non_public_metrics", - "note_tweet", - "organic_metrics", - "possibly_sensitive", - "promoted_metrics", - "public_metrics", - "referenced_tweets", - "reply_settings", - "scopes", - "source", - "text", - "withheld" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Tweet fields to include in the response. Example: ['created_at','author_id','public_metrics']." - }, - "expansions": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "article.cover_media", - "article.media_entities", - "attachments.media_keys", - "attachments.media_source_tweet", - "attachments.poll_ids", - "author_id", - "author_screen_name", - "edit_history_tweet_ids", - "entities.mentions.username", - "entities.note.mentions.username", - "geo.place_id", - "in_reply_to_user_id", - "referenced_tweets.id", - "referenced_tweets.id.author_id" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Expansions to hydrate related objects like users, media, polls, and places." - }, - "media_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "alt_text", - "duration_ms", - "height", - "media_key", - "non_public_metrics", - "organic_metrics", - "preview_image_url", - "promoted_metrics", - "public_metrics", - "type", - "url", - "variants", - "width" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Media fields to include when media keys are expanded." - }, - "place_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "contained_within", - "country", - "country_code", - "full_name", - "geo", - "id", - "name", - "place_type" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Place fields to include when place IDs are expanded." - }, - "poll_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "duration_minutes", - "end_datetime", - "id", - "options", - "voting_status" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Poll fields to include when poll IDs are expanded." - }, - "user_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "affiliation", - "connection_status", - "created_at", - "description", - "entities", - "id", - "location", - "most_recent_tweet_id", - "name", - "pinned_tweet_id", - "profile_banner_url", - "profile_image_url", - "protected", - "public_metrics", - "receives_your_dm", - "subscription_type", - "url", - "verified", - "verified_type", - "withheld", - "username" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "User fields to include when user IDs are expanded. Username is always returned by default." - }, - "since_id": { - "type": "string", - "description": "Return Tweets more recent than this ID (cannot be used with start_time)." - }, - "until_id": { - "type": "string", - "description": "Return Tweets older than this ID (cannot be used with end_time)." - }, - "next_token": { - "type": "string", - "description": "Pagination token from a previous response's meta.next_token." - }, - "pagination_token": { - "type": "string", - "description": "Alternative pagination token from a previous meta.next_token; next_token is preferred." - } - }, - "required": ["query"], - "additionalProperties": false - } - }, - "bash": { - "type": "builtin", - "name": "executeCommand", - "description": "Execute bash commands to manipulate files like tweets.txt, e.g. writing search results to disk or appending logs.", - "inputSchema": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The bash command to execute, such as 'echo \"text\" >> tweets.txt' or 'cat tweets.txt'." - } - }, - "required": ["command"], - "additionalProperties": false - } - } - } - } - -{ - "name": "podcast_transcript_agent", - "description": "An agent that will generate a transcript of a podcast", - "model": "gpt-4.1", - "instructions": "You job is to create a NotebookLM style 1 minute podcast between 2 speakers John and Chloe. Each line should be a new speaker. The podcast should be about the contents of the two papers (that were selected). You can use [sighs], [inhales then exhales], [chuckles], [laughs], [clears throat], [coughs], [sniffs], [pauses] etc. to make the podcast more natural." -} - -{ - "name": "elevenlabs_audio_gen", - "description": "An agent that will generate an audio file from a text", - "model": "gpt-4.1", - "instructions": "Your job is to take the mutli speaker transcript and generate an audio file from it. Use the elevenlabs text to speech tool to do this. For each speaker turn, you should generate an audio file and then combine them all into a single audio file. Use the voice_name 'Liam' for John and 'Cassidy' for Chloe. Make sure to remove the speaker names from the text before generating the audio files. Use the eleven_v3 model_id. In addition, you should use the compose_music tool to generate a short musical intro and outro for the podcast. The intro should be a small 5-10 second clip modeled after popular podcasts which fades and the podcast starts. The outro should be 10-15 seconds of a related sound. Save the intro and outro to files, and then use the bash tool to stitch them with the main podcast audio so that the final output audio file starts with the intro music, then the full conversation, and ends with the outro music. Place all generated audio on the Desktop by default unless otherwise instructed. Don't wait for confirmation - go ahead and produce the podcast.", - "tools": { - "text_to_speech": { - "type": "mcp", - "name": "text_to_speech", - "description": "Generate an audio file from a text", - "mcpServerName": "elevenLabs", - "inputSchema": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "The text to generate an audio file from" - }, - "voice_name": { - "type": "string", - "description": "The voice name to use for the audio file" - }, - "model_id": { - "type": "string", - "description": "The model id to use for the audio file" - } - } - } - }, - "compose_music": { - "type": "mcp", - "name": "compose_music", - "description": "Generate intro and outro music for the podcast and save as audio files", - "mcpServerName": "elevenLabs", - "inputSchema": { - "type": "object", - "properties": { - "prompt": { - "anyOf": [ - {"type": "string"}, - {"type": "null"} - ], - "default": null, - "title": "Prompt" - }, - "output_directory": { - "anyOf": [ - {"type": "string"}, - {"type": "null"} - ], - "default": null, - "title": "Output Directory" - }, - "composition_plan": { - "anyOf": [ - {"$ref": "#/$defs/MusicPrompt"}, - {"type": "null"} - ], - "default": null - }, - "music_length_ms": { - "anyOf": [ - {"type": "integer"}, - {"type": "null"} - ], - "default": null, - "title": "Music Length Ms" - } - }, - "$defs": { - "MusicPrompt": { - "additionalProperties": true, - "properties": { - "positive_global_styles": { - "items": {"type": "string"}, - "title": "Positive Global Styles", - "type": "array" - }, - "negative_global_styles": { - "items": {"type": "string"}, - "title": "Negative Global Styles", - "type": "array" - }, - "sections": { - "items": {"$ref": "#/$defs/SongSection"}, - "title": "Sections", - "type": "array" - } - }, - "required": [ - "positive_global_styles", - "negative_global_styles", - "sections" - ], - "title": "MusicPrompt", - "type": "object" - }, - "SectionSource": { - "additionalProperties": true, - "properties": { - "song_id": { - "title": "Song Id", - "type": "string" - }, - "range": {"$ref": "#/$defs/TimeRange"}, - "negative_ranges": { - "anyOf": [ - { - "items": {"$ref": "#/$defs/TimeRange"}, - "type": "array" - }, - {"type": "null"} - ], - "default": null, - "title": "Negative Ranges" - } - }, - "required": [ - "song_id", - "range" - ], - "title": "SectionSource", - "type": "object" - }, - "SongSection": { - "additionalProperties": true, - "properties": { - "section_name": { - "title": "Section Name", - "type": "string" - }, - "positive_local_styles": { - "items": {"type": "string"}, - "title": "Positive Local Styles", - "type": "array" - }, - "negative_local_styles": { - "items": {"type": "string"}, - "title": "Negative Local Styles", - "type": "array" - }, - "duration_ms": { - "title": "Duration Ms", - "type": "integer" - }, - "lines": { - "items": {"type": "string"}, - "title": "Lines", - "type": "array" - }, - "source_from": { - "anyOf": [ - {"$ref": "#/$defs/SectionSource"}, - {"type": "null"} - ], - "default": null - } - }, - "required": [ - "section_name", - "positive_local_styles", - "negative_local_styles", - "duration_ms", - "lines" - ], - "title": "SongSection", - "type": "object" - }, - "TimeRange": { - "additionalProperties": true, - "properties": { - "start_ms": { - "title": "Start Ms", - "type": "integer" - }, - "end_ms": { - "title": "End Ms", - "type": "integer" - } - }, - "required": [ - "start_ms", - "end_ms" - ], - "title": "TimeRange", - "type": "object" - } - }, - "title": "compose_musicArguments" - } - }, - "bash": { - "type": "builtin", - "name": "executeCommand" - } - } -} - -{ - "mcpServers": { - "elevenLabs": { - "command": "uvx", - "args": [ - "elevenlabs-mcp" - ], - "env": { - "ELEVENLABS_API_KEY": "" - } - }, - "calendar": { - "type": "http", - "url": "" - }, - "twitter": { - "type": "http", - "url": "" - }, - } -} \ No newline at end of file diff --git a/apps/cli/package.json b/apps/cli/package.json index 530c08c5..e1a712e8 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -10,7 +10,8 @@ }, "files": [ "dist", - "bin" + "bin", + "examples" ], "bin": { "rowboatx": "bin/app.js" diff --git a/apps/cli/src/application/entities/mcp.ts b/apps/cli/src/application/entities/mcp.ts index 4f6490cd..e47ebb95 100644 --- a/apps/cli/src/application/entities/mcp.ts +++ b/apps/cli/src/application/entities/mcp.ts @@ -1,16 +1,20 @@ -import z from "zod"; +import { z } from "zod"; -const StdioMcpServerConfig = z.object({ +export const StdioMcpServerConfig = z.object({ + type: z.literal("stdio").optional(), command: z.string(), args: z.array(z.string()).optional(), env: z.record(z.string(), z.string()).optional(), }); -const HttpMcpServerConfig = z.object({ +export const HttpMcpServerConfig = z.object({ + type: z.literal("http").optional(), url: z.string(), headers: z.record(z.string(), z.string()).optional(), }); +export const McpServerDefinition = z.union([StdioMcpServerConfig, HttpMcpServerConfig]); + export const McpServerConfig = z.object({ - mcpServers: z.record(z.string(), z.union([StdioMcpServerConfig, HttpMcpServerConfig])), -}); \ No newline at end of file + mcpServers: z.record(z.string(), McpServerDefinition), +}); diff --git a/apps/cli/src/application/examples/import-example.ts b/apps/cli/src/application/examples/import-example.ts new file mode 100644 index 00000000..31f7fefc --- /dev/null +++ b/apps/cli/src/application/examples/import-example.ts @@ -0,0 +1,115 @@ +import path from "path"; +import { fileURLToPath } from "url"; +import { promises as fs } from "fs"; +import { z } from "zod"; +import { Agent } from "../entities/agent.js"; +import { WorkDir } from "../config/config.js"; +import { McpServerConfig, McpServerDefinition } from "../entities/mcp.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const PackageRoot = path.resolve(__dirname, "../../.."); +const ExamplesDir = path.join(PackageRoot, "examples"); + +const ExampleSchema = z.object({ + id: z.string().min(1), + description: z.string().optional(), + entryAgent: z.string().optional(), + agents: z.array(Agent).min(1), + mcpServers: z.record(z.string(), McpServerDefinition).optional(), +}).refine( + (data) => !data.entryAgent || data.agents.some((agent) => agent.name === data.entryAgent), + { + message: "entryAgent must reference one of the defined agents", + path: ["entryAgent"], + }, +); + +async function readExampleFile(exampleName: string): Promise { + const examplePath = path.join(ExamplesDir, `${exampleName}.json`); + try { + await fs.access(examplePath); + return await fs.readFile(examplePath, "utf8"); + } catch (error) { + const availableExamples = await listAvailableExamples(); + const listMessage = availableExamples.length + ? `Available examples: ${availableExamples.join(", ")}` + : "No packaged examples were found."; + throw new Error(`Unknown example '${exampleName}'. ${listMessage}`); + } +} + +export async function listAvailableExamples(): Promise { + try { + const entries = await fs.readdir(ExamplesDir); + return entries + .filter((entry) => entry.endsWith(".json")) + .map((entry) => entry.replace(/\.json$/, "")) + .sort(); + } catch { + return []; + } +} + +async function writeAgents(agents: z.infer[]) { + await fs.mkdir(path.join(WorkDir, "agents"), { recursive: true }); + await Promise.all( + agents.map(async (agent) => { + const agentPath = path.join(WorkDir, "agents", `${agent.name}.json`); + await fs.writeFile(agentPath, JSON.stringify(agent, null, 2), "utf8"); + }), + ); +} + +async function mergeMcpServers(servers: Record>) { + const result = { added: [] as string[], skipped: [] as string[] }; + if (!servers || Object.keys(servers).length === 0) { + return result; + } + const configPath = path.join(WorkDir, "config", "mcp.json"); + let currentConfig: z.infer = { mcpServers: {} }; + try { + const contents = await fs.readFile(configPath, "utf8"); + currentConfig = McpServerConfig.parse(JSON.parse(contents)); + } catch (error: any) { + if (error?.code !== "ENOENT") { + throw new Error(`Unable to read MCP config: ${error.message ?? error}`); + } + } + let modified = false; + for (const [name, definition] of Object.entries(servers)) { + if (currentConfig.mcpServers[name]) { + result.skipped.push(name); + continue; + } + currentConfig.mcpServers[name] = definition; + result.added.push(name); + modified = true; + } + await fs.mkdir(path.dirname(configPath), { recursive: true }); + if (modified) { + await fs.writeFile(configPath, JSON.stringify(currentConfig, null, 2), "utf8"); + } + return result; +} + +export async function importExample(exampleName: string) { + const raw = await readExampleFile(exampleName); + const parsed = ExampleSchema.parse(JSON.parse(raw)); + const entryAgentName = parsed.entryAgent ?? parsed.agents[0]?.name; + if (!entryAgentName) { + throw new Error(`Example '${exampleName}' does not define any agents to run.`); + } + await writeAgents(parsed.agents); + let serverMerge = { added: [] as string[], skipped: [] as string[] }; + if (parsed.mcpServers) { + serverMerge = await mergeMcpServers(parsed.mcpServers); + } + return { + id: parsed.id, + entryAgent: entryAgentName, + importedAgents: parsed.agents.map((agent) => agent.name), + addedServers: serverMerge.added, + skippedServers: serverMerge.skipped, + }; +} From f2255d219c0d654adfda49c2230e1a7c1e7f72c1 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 20 Nov 2025 17:17:36 +0530 Subject: [PATCH 05/21] changed --example to --sync-example --- apps/cli/bin/app.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 4ba5aa84..e2bb85c3 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -27,14 +27,14 @@ yargs(hideBin(process.argv)) description: "Do not interact with the user", default: false, }) - .option("example", { + .option("sync-examples", { type: "string", description: "Import an example workflow by name (use 'all' for every example) before running", }), async (argv) => { let agent = argv.agent ?? "copilot"; - if (argv.example) { - const requested = String(argv.example).trim(); + if (argv["sync-examples"]) { + const requested = String(argv["sync-examples"]).trim(); const isAll = requested.toLowerCase() === "all"; try { const examplesToImport = isAll ? await listAvailableExamples() : [requested]; @@ -58,7 +58,7 @@ yargs(hideBin(process.argv)) console.error(error?.message ?? error); process.exit(1); } - console.error("Examples imported. Re-run rowboatx without --example (or with --agent ) when you're ready to chat."); + console.error("Examples imported. Re-run rowboatx without --sync-examples (or with --agent ) when you're ready to chat."); return; } await app({ From 55d9f800745ebd42bbd5b1062dcd09597f0c171d Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 20 Nov 2025 17:34:06 +0530 Subject: [PATCH 06/21] rename sync-examples option to sync-example in CLI --- apps/cli/bin/app.js | 8 ++++---- apps/cli/package-lock.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 14e71af9..17a8c443 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -27,14 +27,14 @@ yargs(hideBin(process.argv)) description: "Do not interact with the user", default: false, }) - .option("sync-examples", { + .option("sync-example", { type: "string", description: "Import an example workflow by name (use 'all' for every example) before running", }), async (argv) => { let agent = argv.agent ?? "copilot"; - if (argv["sync-examples"]) { - const requested = String(argv["sync-examples"]).trim(); + if (argv["sync-example"]) { + const requested = String(argv["sync-example"]).trim(); const isAll = requested.toLowerCase() === "all"; try { const examplesToImport = isAll ? await listAvailableExamples() : [requested]; @@ -58,7 +58,7 @@ yargs(hideBin(process.argv)) console.error(error?.message ?? error); process.exit(1); } - console.error("Examples imported. Re-run rowboatx without --sync-examples (or with --agent ) when you're ready to chat."); + console.error("Examples imported. Re-run rowboatx without --sync-example (or with --agent ) when you're ready to chat."); return; } await app({ diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index 5a7c8a43..5b2183f7 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rowboatlabs/rowboatx", - "version": "0.10.0", + "version": "0.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rowboatlabs/rowboatx", - "version": "0.10.0", + "version": "0.12.0", "license": "Apache-2.0", "dependencies": { "@ai-sdk/anthropic": "^2.0.44", From 532065dc0b1f3041d8069258209a4bac9fac99c6 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 10:32:24 +0530 Subject: [PATCH 07/21] fix: sync-example implementation --- apps/cli/bin/app.js | 98 ++++++++----- apps/cli/examples/twitter-podcast.json | 1 + apps/cli/src/app.ts | 133 +++++++++++++++++- .../application/examples/import-example.ts | 115 --------------- 4 files changed, 197 insertions(+), 150 deletions(-) delete mode 100644 apps/cli/src/application/examples/import-example.ts diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 17a8c443..5a271571 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -1,8 +1,7 @@ #!/usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { app, modelConfig, updateState } from '../dist/app.js'; -import { importExample, listAvailableExamples } from '../dist/application/examples/import-example.js'; +import { app, modelConfig, updateState, importExample, listExamples } from '../dist/app.js'; yargs(hideBin(process.argv)) @@ -26,41 +25,9 @@ yargs(hideBin(process.argv)) type: "boolean", description: "Do not interact with the user", default: false, - }) - .option("sync-example", { - type: "string", - description: "Import an example workflow by name (use 'all' for every example) before running", }), async (argv) => { let agent = argv.agent ?? "copilot"; - if (argv["sync-example"]) { - const requested = String(argv["sync-example"]).trim(); - const isAll = requested.toLowerCase() === "all"; - try { - const examplesToImport = isAll ? await listAvailableExamples() : [requested]; - if (examplesToImport.length === 0) { - console.error("No packaged examples are available to import."); - process.exit(1); - } - for (const exampleName of examplesToImport) { - const imported = await importExample(exampleName); - const agentList = imported.importedAgents.join(", "); - console.error(`Imported example '${exampleName}' with agents: ${agentList}`); - console.error(`Primary agent: ${imported.entryAgent}`); - if (imported.addedServers.length > 0) { - console.error(`Configured new MCP servers: ${imported.addedServers.join(", ")}`); - } - if (imported.skippedServers.length > 0) { - console.error(`Skipped existing MCP servers (already configured): ${imported.skippedServers.join(", ")}`); - } - } - } catch (error) { - console.error(error?.message ?? error); - process.exit(1); - } - console.error("Examples imported. Re-run rowboatx without --sync-example (or with --agent ) when you're ready to chat."); - return; - } await app({ agent, runId: argv.run_id, @@ -69,6 +36,69 @@ yargs(hideBin(process.argv)) }); } ) + .command( + "sync-example ", + "Import an example workflow by name", + (y) => y.positional("example", { + type: "string", + description: "The example to import", + }), + async (argv) => { + const exampleName = String(argv.example).trim(); + try { + const imported = await importExample(exampleName); + + // Build output message + const output = [ + `✓ Imported example '${exampleName}'`, + ` Agents: ${imported.importedAgents.join(", ")}`, + ` Primary: ${imported.entryAgent}`, + ]; + + if (imported.addedServers.length > 0) { + output.push(` MCP servers added: ${imported.addedServers.join(", ")}`); + } + if (imported.skippedServers.length > 0) { + output.push(` MCP servers skipped (already configured): ${imported.skippedServers.join(", ")}`); + } + + console.log(output.join("\n")); + + if (imported.postInstallInstructions) { + console.log("\n" + "=".repeat(60)); + console.log("POST-INSTALL INSTRUCTIONS"); + console.log("=".repeat(60)); + console.log(imported.postInstallInstructions); + console.log("=".repeat(60) + "\n"); + } + + console.log(`\nRun: rowboatx --agent ${imported.entryAgent}`); + } catch (error) { + console.error("Error:", error?.message ?? error); + process.exit(1); + } + } + ) + .command( + "list-example", + "List all available example workflows", + (y) => y, + async () => { + try { + const examples = await listExamples(); + if (examples.length === 0) { + console.error("No packaged examples are available to list."); + return; + } + for (const example of examples) { + console.log(example); + } + } catch (error) { + console.error(error?.message ?? error); + process.exit(1); + } + } + ) .command( "model-config", "Select model", diff --git a/apps/cli/examples/twitter-podcast.json b/apps/cli/examples/twitter-podcast.json index d008fcb6..28420c98 100644 --- a/apps/cli/examples/twitter-podcast.json +++ b/apps/cli/examples/twitter-podcast.json @@ -1,5 +1,6 @@ { "id": "twitter-podcast", + "post-import-instructions": "This example workflow generates a narrated podcast episode from recent AI-related tweets using multiple agents.", "description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.", "entryAgent": "tweet-podcast", "agents": [ diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts index 7f384365..0cc1d490 100644 --- a/apps/cli/src/app.ts +++ b/apps/cli/src/app.ts @@ -2,13 +2,22 @@ import { AgentState, streamAgent } from "./application/lib/agent.js"; import { StreamRenderer } from "./application/lib/stream-renderer.js"; import { stdin as input, stdout as output } from "node:process"; import fs from "fs"; +import { promises as fsp } from "fs"; import path from "path"; +import { fileURLToPath } from "url"; import { WorkDir, getModelConfig, updateModelConfig } from "./application/config/config.js"; import { RunEvent } from "./application/entities/run-events.js"; import { createInterface, Interface } from "node:readline/promises"; import { ToolCallPart } from "./application/entities/message.js"; +import { Agent } from "./application/entities/agent.js"; +import { McpServerConfig, McpServerDefinition } from "./application/entities/mcp.js"; import { z } from "zod"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const PackageRoot = path.resolve(__dirname, ".."); +const ExamplesDir = path.join(PackageRoot, "examples"); + export async function updateState(agent: string, runId: string) { const state = new AgentState(agent, runId); // If running in a TTY, read run events from stdin line-by-line @@ -386,4 +395,126 @@ function renderCurrentModel(provider: string, flavor: string, model: string) { console.log(`- provider: ${provider}${flavor ? ` (${flavor})` : ""}`); console.log(`- model: ${model}`); console.log(""); -} \ No newline at end of file +} + +const ExampleSchema = z.object({ + id: z.string().min(1), + "post-install-instructions": z.string().optional(), + description: z.string().optional(), + entryAgent: z.string().optional(), + agents: z.array(Agent).min(1), + mcpServers: z.record(z.string(), McpServerDefinition).optional(), +}).refine( + (data) => !data.entryAgent || data.agents.some((agent) => agent.name === data.entryAgent), + { + message: "entryAgent must reference one of the defined agents", + path: ["entryAgent"], + }, +); + +async function readExampleFile(exampleName: string): Promise { + const examplePath = path.join(ExamplesDir, `${exampleName}.json`); + try { + return await fsp.readFile(examplePath, "utf8"); + } catch (error: any) { + if (error?.code === "ENOENT") { + const availableExamples = await listAvailableExamples(); + const listMessage = availableExamples.length + ? `Available examples: ${availableExamples.join(", ")}` + : "No packaged examples were found."; + throw new Error(`Unknown example '${exampleName}'. ${listMessage}`); + } + // Re-throw other errors (permission issues, etc.) + throw error; + } +} + +async function listAvailableExamples(): Promise { + try { + const entries = await fsp.readdir(ExamplesDir); + return entries + .filter((entry) => entry.endsWith(".json")) + .map((entry) => entry.replace(/\.json$/, "")) + .sort(); + } catch { + return []; + } +} + +async function writeAgents(agents: z.infer[]) { + await fsp.mkdir(path.join(WorkDir, "agents"), { recursive: true }); + await Promise.all( + agents.map(async (agent) => { + const agentPath = path.join(WorkDir, "agents", `${agent.name}.json`); + await fsp.writeFile(agentPath, JSON.stringify(agent, null, 2), "utf8"); + }), + ); +} + +async function mergeMcpServers(servers: Record>) { + const result = { added: [] as string[], skipped: [] as string[] }; + + // Early return if no servers to process + if (!servers || Object.keys(servers).length === 0) { + return result; + } + + const configPath = path.join(WorkDir, "config", "mcp.json"); + + // Read existing config + let currentConfig: z.infer = { mcpServers: {} }; + try { + const contents = await fsp.readFile(configPath, "utf8"); + currentConfig = McpServerConfig.parse(JSON.parse(contents)); + } catch (error: any) { + if (error?.code !== "ENOENT") { + throw new Error(`Unable to read MCP config: ${error.message ?? error}`); + } + // File doesn't exist yet, use empty config + } + + // Merge servers + for (const [name, definition] of Object.entries(servers)) { + if (currentConfig.mcpServers[name]) { + result.skipped.push(name); + } else { + currentConfig.mcpServers[name] = definition; + result.added.push(name); + } + } + + // Only write if we added new servers + if (result.added.length > 0) { + await fsp.mkdir(path.dirname(configPath), { recursive: true }); + await fsp.writeFile(configPath, JSON.stringify(currentConfig, null, 2), "utf8"); + } + + return result; +} + +export async function importExample(exampleName: string) { + const raw = await readExampleFile(exampleName); + const parsed = ExampleSchema.parse(JSON.parse(raw)); + const entryAgentName = parsed.entryAgent ?? parsed.agents[0]?.name; + if (!entryAgentName) { + throw new Error(`Example '${exampleName}' does not define any agents to run.`); + } + const postInstallInstructions = parsed["post-install-instructions"]; + await writeAgents(parsed.agents); + let serverMerge = { added: [] as string[], skipped: [] as string[] }; + if (parsed.mcpServers) { + serverMerge = await mergeMcpServers(parsed.mcpServers); + } + return { + id: parsed.id, + entryAgent: entryAgentName, + importedAgents: parsed.agents.map((agent) => agent.name), + addedServers: serverMerge.added, + skippedServers: serverMerge.skipped, + postInstallInstructions, + }; +} + +export async function listExamples() { + return listAvailableExamples(); +} diff --git a/apps/cli/src/application/examples/import-example.ts b/apps/cli/src/application/examples/import-example.ts deleted file mode 100644 index 31f7fefc..00000000 --- a/apps/cli/src/application/examples/import-example.ts +++ /dev/null @@ -1,115 +0,0 @@ -import path from "path"; -import { fileURLToPath } from "url"; -import { promises as fs } from "fs"; -import { z } from "zod"; -import { Agent } from "../entities/agent.js"; -import { WorkDir } from "../config/config.js"; -import { McpServerConfig, McpServerDefinition } from "../entities/mcp.js"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const PackageRoot = path.resolve(__dirname, "../../.."); -const ExamplesDir = path.join(PackageRoot, "examples"); - -const ExampleSchema = z.object({ - id: z.string().min(1), - description: z.string().optional(), - entryAgent: z.string().optional(), - agents: z.array(Agent).min(1), - mcpServers: z.record(z.string(), McpServerDefinition).optional(), -}).refine( - (data) => !data.entryAgent || data.agents.some((agent) => agent.name === data.entryAgent), - { - message: "entryAgent must reference one of the defined agents", - path: ["entryAgent"], - }, -); - -async function readExampleFile(exampleName: string): Promise { - const examplePath = path.join(ExamplesDir, `${exampleName}.json`); - try { - await fs.access(examplePath); - return await fs.readFile(examplePath, "utf8"); - } catch (error) { - const availableExamples = await listAvailableExamples(); - const listMessage = availableExamples.length - ? `Available examples: ${availableExamples.join(", ")}` - : "No packaged examples were found."; - throw new Error(`Unknown example '${exampleName}'. ${listMessage}`); - } -} - -export async function listAvailableExamples(): Promise { - try { - const entries = await fs.readdir(ExamplesDir); - return entries - .filter((entry) => entry.endsWith(".json")) - .map((entry) => entry.replace(/\.json$/, "")) - .sort(); - } catch { - return []; - } -} - -async function writeAgents(agents: z.infer[]) { - await fs.mkdir(path.join(WorkDir, "agents"), { recursive: true }); - await Promise.all( - agents.map(async (agent) => { - const agentPath = path.join(WorkDir, "agents", `${agent.name}.json`); - await fs.writeFile(agentPath, JSON.stringify(agent, null, 2), "utf8"); - }), - ); -} - -async function mergeMcpServers(servers: Record>) { - const result = { added: [] as string[], skipped: [] as string[] }; - if (!servers || Object.keys(servers).length === 0) { - return result; - } - const configPath = path.join(WorkDir, "config", "mcp.json"); - let currentConfig: z.infer = { mcpServers: {} }; - try { - const contents = await fs.readFile(configPath, "utf8"); - currentConfig = McpServerConfig.parse(JSON.parse(contents)); - } catch (error: any) { - if (error?.code !== "ENOENT") { - throw new Error(`Unable to read MCP config: ${error.message ?? error}`); - } - } - let modified = false; - for (const [name, definition] of Object.entries(servers)) { - if (currentConfig.mcpServers[name]) { - result.skipped.push(name); - continue; - } - currentConfig.mcpServers[name] = definition; - result.added.push(name); - modified = true; - } - await fs.mkdir(path.dirname(configPath), { recursive: true }); - if (modified) { - await fs.writeFile(configPath, JSON.stringify(currentConfig, null, 2), "utf8"); - } - return result; -} - -export async function importExample(exampleName: string) { - const raw = await readExampleFile(exampleName); - const parsed = ExampleSchema.parse(JSON.parse(raw)); - const entryAgentName = parsed.entryAgent ?? parsed.agents[0]?.name; - if (!entryAgentName) { - throw new Error(`Example '${exampleName}' does not define any agents to run.`); - } - await writeAgents(parsed.agents); - let serverMerge = { added: [] as string[], skipped: [] as string[] }; - if (parsed.mcpServers) { - serverMerge = await mergeMcpServers(parsed.mcpServers); - } - return { - id: parsed.id, - entryAgent: entryAgentName, - importedAgents: parsed.agents.map((agent) => agent.name), - addedServers: serverMerge.added, - skippedServers: serverMerge.skipped, - }; -} From 97e47faca89e1cf00475faa96ee219aa3fafbbd7 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:42:57 +0530 Subject: [PATCH 08/21] refactor example import --- apps/cli/package.json | 3 +- apps/cli/src/app.ts | 77 ++++--------------- apps/cli/src/application/config/config.ts | 13 ---- apps/cli/src/application/entities/example.ts | 12 +++ apps/cli/src/examples/gemini3-test.json | 8 ++ apps/cli/src/examples/index.ts | 9 +++ .../{ => src}/examples/twitter-podcast.json | 2 +- 7 files changed, 46 insertions(+), 78 deletions(-) create mode 100644 apps/cli/src/application/entities/example.ts create mode 100644 apps/cli/src/examples/gemini3-test.json create mode 100644 apps/cli/src/examples/index.ts rename apps/cli/{ => src}/examples/twitter-podcast.json (99%) diff --git a/apps/cli/package.json b/apps/cli/package.json index 77e2ed3a..94adbfcf 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -10,8 +10,7 @@ }, "files": [ "dist", - "bin", - "examples" + "bin" ], "bin": { "rowboatx": "bin/app.js" diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts index 634923bf..6740321c 100644 --- a/apps/cli/src/app.ts +++ b/apps/cli/src/app.ts @@ -4,7 +4,6 @@ import { stdin as input, stdout as output } from "node:process"; import fs from "fs"; import { promises as fsp } from "fs"; import path from "path"; -import { fileURLToPath } from "url"; import { WorkDir, getModelConfig, updateModelConfig } from "./application/config/config.js"; import { RunEvent } from "./application/entities/run-events.js"; import { createInterface, Interface } from "node:readline/promises"; @@ -12,12 +11,8 @@ import { ToolCallPart } from "./application/entities/message.js"; import { Agent } from "./application/entities/agent.js"; import { McpServerConfig, McpServerDefinition } from "./application/entities/mcp.js"; import { z } from "zod"; -import { Flavor, ModelConfig } from "./application/entities/models.js"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const PackageRoot = path.resolve(__dirname, ".."); -const ExamplesDir = path.join(PackageRoot, "examples"); +import { Flavor } from "./application/entities/models.js"; +import { examples } from "./examples/index.js"; export async function updateState(agent: string, runId: string) { const state = new AgentState(agent, runId); @@ -413,51 +408,14 @@ function renderCurrentModel(provider: string, flavor: string, model: string) { console.log(""); } -const ExampleSchema = z.object({ - id: z.string().min(1), - "post-install-instructions": z.string().optional(), - description: z.string().optional(), - entryAgent: z.string().optional(), - agents: z.array(Agent).min(1), - mcpServers: z.record(z.string(), McpServerDefinition).optional(), -}).refine( - (data) => !data.entryAgent || data.agents.some((agent) => agent.name === data.entryAgent), - { - message: "entryAgent must reference one of the defined agents", - path: ["entryAgent"], - }, -); - -async function readExampleFile(exampleName: string): Promise { - const examplePath = path.join(ExamplesDir, `${exampleName}.json`); - try { - return await fsp.readFile(examplePath, "utf8"); - } catch (error: any) { - if (error?.code === "ENOENT") { - const availableExamples = await listAvailableExamples(); - const listMessage = availableExamples.length - ? `Available examples: ${availableExamples.join(", ")}` - : "No packaged examples were found."; - throw new Error(`Unknown example '${exampleName}'. ${listMessage}`); - } - // Re-throw other errors (permission issues, etc.) - throw error; - } -} - async function listAvailableExamples(): Promise { - try { - const entries = await fsp.readdir(ExamplesDir); - return entries - .filter((entry) => entry.endsWith(".json")) - .map((entry) => entry.replace(/\.json$/, "")) - .sort(); - } catch { - return []; - } + return Object.keys(examples); } -async function writeAgents(agents: z.infer[]) { +async function writeAgents(agents: z.infer[] | undefined) { + if (!agents) { + return; + } await fsp.mkdir(path.join(WorkDir, "agents"), { recursive: true }); await Promise.all( agents.map(async (agent) => { @@ -509,22 +467,17 @@ async function mergeMcpServers(servers: Record agent.name), + id: example.id, + entryAgent: example.entryAgent, + importedAgents: example.agents?.map((agent) => agent.name) ?? [], addedServers: serverMerge.added, skippedServers: serverMerge.skipped, postInstallInstructions, diff --git a/apps/cli/src/application/config/config.ts b/apps/cli/src/application/config/config.ts index e0d6b27c..4702a765 100644 --- a/apps/cli/src/application/config/config.ts +++ b/apps/cli/src/application/config/config.ts @@ -12,19 +12,6 @@ let modelConfig: z.infer | null = null; const baseMcpConfig: z.infer = { mcpServers: { - firecrawl: { - command: "npx", - args: ["-y", "supergateway", "--stdio", "npx -y firecrawl-mcp"], - env: { - FIRECRAWL_API_KEY: "fc-aaacee4bdd164100a4d83af85bef6fdc", - }, - }, - test: { - url: "http://localhost:3000", - headers: { - "Authorization": "Bearer test", - }, - }, } }; diff --git a/apps/cli/src/application/entities/example.ts b/apps/cli/src/application/entities/example.ts new file mode 100644 index 00000000..8857a55c --- /dev/null +++ b/apps/cli/src/application/entities/example.ts @@ -0,0 +1,12 @@ +import z from "zod" +import { Agent } from "./agent.js" +import { McpServerDefinition } from "./mcp.js" + +export const Example = z.object({ + id: z.string(), + instructions: z.string().optional(), + description: z.string().optional(), + entryAgent: z.string().optional(), + agents: z.array(Agent).optional(), + mcpServers: z.record(z.string(), McpServerDefinition).optional(), +}); \ No newline at end of file diff --git a/apps/cli/src/examples/gemini3-test.json b/apps/cli/src/examples/gemini3-test.json new file mode 100644 index 00000000..54f63c38 --- /dev/null +++ b/apps/cli/src/examples/gemini3-test.json @@ -0,0 +1,8 @@ +{ + "id": "gemini3_svg_pelican", + "provider": "google", + "model": "gemini-3.0-pro", + "description": "Outputs a single valid SVG depicting a pelican riding a bicycle.", + "instructions": "You must output only a single, valid, self-contained SVG XML depicting a pelican riding a bicycle. Requirements: 1) Output must be ONLY raw SVG XML (no markdown fences, no explanations). 2) Use viewBox=\"0 0 512 512\" and set width/height to 512. 3) Include clear, recognizable pelican and bicycle using basic shapes/paths. 4) No external refs, images, scripts, or styles; use inline attributes only. 5) Keep IDs minimal; keep total file size reasonable.", + "tools": {} +} \ No newline at end of file diff --git a/apps/cli/src/examples/index.ts b/apps/cli/src/examples/index.ts new file mode 100644 index 00000000..83abf58e --- /dev/null +++ b/apps/cli/src/examples/index.ts @@ -0,0 +1,9 @@ +import twitterPodcast from './twitter-podcast.json' with { type: 'json' }; +import gemini3Test from './gemini3-test.json' with { type: 'json' }; +import { Example } from '../application/entities/example.js'; +import z from 'zod'; + +export const examples: Record> = { + "twitter-podcast": Example.parse(twitterPodcast), + "gemini3-test": Example.parse(gemini3Test), +}; \ No newline at end of file diff --git a/apps/cli/examples/twitter-podcast.json b/apps/cli/src/examples/twitter-podcast.json similarity index 99% rename from apps/cli/examples/twitter-podcast.json rename to apps/cli/src/examples/twitter-podcast.json index 28420c98..05f2c136 100644 --- a/apps/cli/examples/twitter-podcast.json +++ b/apps/cli/src/examples/twitter-podcast.json @@ -1,6 +1,6 @@ { "id": "twitter-podcast", - "post-import-instructions": "This example workflow generates a narrated podcast episode from recent AI-related tweets using multiple agents.", + "instructions": "This example workflow generates a narrated podcast episode from recent AI-related tweets using multiple agents.", "description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.", "entryAgent": "tweet-podcast", "agents": [ From 014c8a56f5c69f35ece758f7ed28ca507175b266 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:43:59 +0530 Subject: [PATCH 09/21] fix yargs --- apps/cli/bin/app.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 5a271571..87396adc 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -11,7 +11,8 @@ yargs(hideBin(process.argv)) (y) => y .option("agent", { type: "string", - description: "The agent to run (defaults to copilot)", + description: "The agent to run", + default: "copilot", }) .option("run_id", { type: "string", @@ -26,10 +27,9 @@ yargs(hideBin(process.argv)) description: "Do not interact with the user", default: false, }), - async (argv) => { - let agent = argv.agent ?? "copilot"; - await app({ - agent, + (argv) => { + app({ + agent: argv.agent, runId: argv.run_id, input: argv.input, noInteractive: argv.noInteractive, @@ -47,23 +47,23 @@ yargs(hideBin(process.argv)) const exampleName = String(argv.example).trim(); try { const imported = await importExample(exampleName); - + // Build output message const output = [ `✓ Imported example '${exampleName}'`, ` Agents: ${imported.importedAgents.join(", ")}`, ` Primary: ${imported.entryAgent}`, ]; - + if (imported.addedServers.length > 0) { output.push(` MCP servers added: ${imported.addedServers.join(", ")}`); } if (imported.skippedServers.length > 0) { output.push(` MCP servers skipped (already configured): ${imported.skippedServers.join(", ")}`); } - + console.log(output.join("\n")); - + if (imported.postInstallInstructions) { console.log("\n" + "=".repeat(60)); console.log("POST-INSTALL INSTRUCTIONS"); @@ -71,7 +71,7 @@ yargs(hideBin(process.argv)) console.log(imported.postInstallInstructions); console.log("=".repeat(60) + "\n"); } - + console.log(`\nRun: rowboatx --agent ${imported.entryAgent}`); } catch (error) { console.error("Error:", error?.message ?? error); From 55097817c6c70de72e7c243801e463d9e86d9d80 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 13:14:20 +0530 Subject: [PATCH 10/21] fix: - remove changes to package-lock - remove output messages from app.js and moved them into importExample --- apps/cli/bin/app.js | 29 +--------------------- apps/cli/package-lock.json | 6 ++--- apps/cli/src/app.ts | 50 +++++++++++++++++++++++++++++++------- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 87396adc..41fdcc9e 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -44,35 +44,8 @@ yargs(hideBin(process.argv)) description: "The example to import", }), async (argv) => { - const exampleName = String(argv.example).trim(); try { - const imported = await importExample(exampleName); - - // Build output message - const output = [ - `✓ Imported example '${exampleName}'`, - ` Agents: ${imported.importedAgents.join(", ")}`, - ` Primary: ${imported.entryAgent}`, - ]; - - if (imported.addedServers.length > 0) { - output.push(` MCP servers added: ${imported.addedServers.join(", ")}`); - } - if (imported.skippedServers.length > 0) { - output.push(` MCP servers skipped (already configured): ${imported.skippedServers.join(", ")}`); - } - - console.log(output.join("\n")); - - if (imported.postInstallInstructions) { - console.log("\n" + "=".repeat(60)); - console.log("POST-INSTALL INSTRUCTIONS"); - console.log("=".repeat(60)); - console.log(imported.postInstallInstructions); - console.log("=".repeat(60) + "\n"); - } - - console.log(`\nRun: rowboatx --agent ${imported.entryAgent}`); + await importExample(String(argv.example).trim()); } catch (error) { console.error("Error:", error?.message ?? error); process.exit(1); diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index 5b2183f7..0df55c71 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rowboatlabs/rowboatx", - "version": "0.12.0", + "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rowboatlabs/rowboatx", - "version": "0.12.0", + "version": "0.10.0", "license": "Apache-2.0", "dependencies": { "@ai-sdk/anthropic": "^2.0.44", @@ -1779,4 +1779,4 @@ } } } -} +} \ No newline at end of file diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts index 6740321c..577f8ca9 100644 --- a/apps/cli/src/app.ts +++ b/apps/cli/src/app.ts @@ -467,21 +467,53 @@ async function mergeMcpServers(servers: Record agent.name) ?? [], - addedServers: serverMerge.added, - skippedServers: serverMerge.skipped, - postInstallInstructions, - }; + + // Build and display output message + const importedAgents = example.agents?.map((agent) => agent.name) ?? []; + const entryAgent = example.entryAgent ?? importedAgents[0] ?? ""; + + const output = [ + `✓ Imported example '${exampleName}'`, + ` Agents: ${importedAgents.join(", ")}`, + ` Primary: ${entryAgent}`, + ]; + + if (serverMerge.added.length > 0) { + output.push(` MCP servers added: ${serverMerge.added.join(", ")}`); + } + if (serverMerge.skipped.length > 0) { + output.push(` MCP servers skipped (already configured): ${serverMerge.skipped.join(", ")}`); + } + + console.log(output.join("\n")); + + // Display post-install instructions if present + if (example.instructions) { + console.log("\n" + "=".repeat(60)); + console.log("POST-INSTALL INSTRUCTIONS"); + console.log("=".repeat(60)); + console.log(example.instructions); + console.log("=".repeat(60) + "\n"); + } + + // Display next steps + console.log(`\nRun: rowboatx --agent ${entryAgent}`); } export async function listExamples() { From 7425e077f37b54fa66e61344a94334a91631bce4 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 13:35:35 +0530 Subject: [PATCH 11/21] fix: restore package-lock.json to match main (remove diff) --- apps/cli/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index 0df55c71..5a7c8a43 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -1779,4 +1779,4 @@ } } } -} \ No newline at end of file +} From 64a284ccc19890bf28a16756a1591b198573e608 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 13:36:53 +0530 Subject: [PATCH 12/21] fix: naming of the commands --- apps/cli/bin/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 41fdcc9e..5fdf3ffc 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -37,7 +37,7 @@ yargs(hideBin(process.argv)) } ) .command( - "sync-example ", + "import-example ", "Import an example workflow by name", (y) => y.positional("example", { type: "string", @@ -53,7 +53,7 @@ yargs(hideBin(process.argv)) } ) .command( - "list-example", + "list-examples", "List all available example workflows", (y) => y, async () => { From 58fa1b91c31e127e52ddaedc590f47a5a2bbab0b Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 15:54:22 +0530 Subject: [PATCH 13/21] update: made import-example into import and it can import example workflows or user made workflows --- apps/cli/bin/app.js | 32 +++++++++++++++++++++++++------- apps/cli/src/app.ts | 43 +++++++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 5fdf3ffc..8be66d23 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -37,15 +37,33 @@ yargs(hideBin(process.argv)) } ) .command( - "import-example ", - "Import an example workflow by name", - (y) => y.positional("example", { - type: "string", - description: "The example to import", - }), + "import", + "Import an example workflow (--example) or custom workflow from file (--file)", + (y) => y + .option("example", { + type: "string", + description: "Name of built-in example to import", + }) + .option("file", { + type: "string", + description: "Path to custom workflow JSON file", + }) + .check((argv) => { + if (!argv.example && !argv.file) { + throw new Error("Either --example or --file must be provided"); + } + if (argv.example && argv.file) { + throw new Error("Cannot use both --example and --file at the same time"); + } + return true; + }), async (argv) => { try { - await importExample(String(argv.example).trim()); + if (argv.example) { + await importExample(String(argv.example).trim()); + } else if (argv.file) { + await importExample(undefined, String(argv.file).trim()); + } } catch (error) { console.error("Error:", error?.message ?? error); process.exit(1); diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts index 577f8ca9..e4d734ea 100644 --- a/apps/cli/src/app.ts +++ b/apps/cli/src/app.ts @@ -10,6 +10,7 @@ import { createInterface, Interface } from "node:readline/promises"; import { ToolCallPart } from "./application/entities/message.js"; import { Agent } from "./application/entities/agent.js"; import { McpServerConfig, McpServerDefinition } from "./application/entities/mcp.js"; +import { Example } from "./application/entities/example.js"; import { z } from "zod"; import { Flavor } from "./application/entities/models.js"; import { examples } from "./examples/index.js"; @@ -466,15 +467,37 @@ async function mergeMcpServers(servers: Record; + let sourceName: string; + + if (exampleName) { + // Load from built-in examples + example = examples[exampleName]; + if (!example) { + const availableExamples = Object.keys(examples); + const listMessage = availableExamples.length + ? `Available examples: ${availableExamples.join(", ")}` + : "No packaged examples are available."; + throw new Error(`Unknown example '${exampleName}'. ${listMessage}`); + } + sourceName = exampleName; + } else if (filePath) { + // Load from file path + try { + const fileContent = await fsp.readFile(filePath, "utf8"); + example = Example.parse(JSON.parse(fileContent)); + sourceName = path.basename(filePath, ".json"); + } catch (error: any) { + if (error?.code === "ENOENT") { + throw new Error(`File not found: ${filePath}`); + } else if (error?.name === "ZodError") { + throw new Error(`Invalid workflow file format: ${error.message}`); + } + throw new Error(`Failed to read workflow file: ${error.message ?? error}`); + } + } else { + throw new Error("Either exampleName or filePath must be provided"); } // Import agents and MCP servers @@ -489,7 +512,7 @@ export async function importExample(exampleName: string) { const entryAgent = example.entryAgent ?? importedAgents[0] ?? ""; const output = [ - `✓ Imported example '${exampleName}'`, + `✓ Imported workflow '${sourceName}'`, ` Agents: ${importedAgents.join(", ")}`, ` Primary: ${entryAgent}`, ]; From c562248850fd20df4e902996a0d8cbf634810f63 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 16:24:23 +0530 Subject: [PATCH 14/21] update: added export capability --- apps/cli/bin/app.js | 20 +- apps/cli/podcast.json | 541 ++++++++++++++++++++++++++++++++++++++++++ apps/cli/src/app.ts | 78 ++++++ 3 files changed, 638 insertions(+), 1 deletion(-) create mode 100644 apps/cli/podcast.json diff --git a/apps/cli/bin/app.js b/apps/cli/bin/app.js index 8be66d23..3eeace50 100755 --- a/apps/cli/bin/app.js +++ b/apps/cli/bin/app.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { app, modelConfig, updateState, importExample, listExamples } from '../dist/app.js'; +import { app, modelConfig, updateState, importExample, listExamples, exportWorkflow } from '../dist/app.js'; yargs(hideBin(process.argv)) @@ -90,6 +90,24 @@ yargs(hideBin(process.argv)) } } ) + .command( + "export", + "Export a workflow with all dependencies (outputs to stdout)", + (y) => y + .option("agent", { + type: "string", + description: "Entry agent name to export", + demandOption: true, + }), + async (argv) => { + try { + await exportWorkflow(String(argv.agent).trim()); + } catch (error) { + console.error("Error:", error?.message ?? error); + process.exit(1); + } + } + ) .command( "model-config", "Select model", diff --git a/apps/cli/podcast.json b/apps/cli/podcast.json new file mode 100644 index 00000000..0321d90f --- /dev/null +++ b/apps/cli/podcast.json @@ -0,0 +1,541 @@ +{ + "id": "tweet-podcast", + "instructions": "This example workflow generates a narrated podcast episode from recent AI-related tweets using multiple agents.", + "description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.", + "entryAgent": "tweet-podcast", + "agents": [ + { + "name": "tweet-podcast", + "model": "gpt-5.1", + "description": "An agent that will produce a podcast from recent tweets", + "instructions": "You are the orchestrator for producing a short podcast episode end-to-end. Follow these steps in order and only advance once each step succeeds:\n\n1. Tweets: call the tweets workflow to collect the latest tweets, .\n\n2.Transcript creation: Provide the resulting tweets to the podcast_transcript_agent tool so it can script a ~1 minute alternating dialogue between John and Chloe that references the tweets and a balanced conversation about AI bubble.\n\n4. Audio production: Send the transcript to the elevenlabs_audio_gen tool create an audio file.", + "tools": { + "tweets": { + "name": "tweets", + "type": "agent" + }, + "podcast_transcript_agent": { + "name": "podcast_transcript_agent", + "type": "agent" + }, + "elevenlabs_audio_gen": { + "name": "elevenlabs_audio_gen", + "type": "agent" + } + } + }, + { + "name": "tweets", + "model": "gpt-4.1", + "description": "Checks latest tweets", + "instructions": "Pulls the recent 10 recent tweets each on OpenAI, Anthropic, Nvidia, Grok, Gemini", + "tools": { + "search_tweets": { + "name": "TWITTER_RECENT_SEARCH", + "type": "mcp", + "description": "Search recent Tweets from the last 7 days using X/Twitter's search syntax via Composio's Twitter MCP server.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query for matching Tweets. Use X search operators like from:username, -is:retweet, -is:reply, has:media, lang:en, etc. Limited to last 7 days." + }, + "start_time": { + "type": "string", + "description": "Oldest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results, within the last 7 days." + }, + "end_time": { + "type": "string", + "description": "Newest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results; exclusive." + }, + "max_results": { + "type": "integer", + "description": "Number of Tweets to return (up to 2000 per call).", + "default": 10 + }, + "sort_order": { + "type": "string", + "enum": [ + "recency", + "relevancy" + ], + "description": "Order of results: 'recency' (most recent first) or 'relevancy'." + }, + "tweet_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "article", + "attachments", + "author_id", + "card_uri", + "context_annotations", + "conversation_id", + "created_at", + "edit_controls", + "edit_history_tweet_ids", + "entities", + "geo", + "id", + "in_reply_to_user_id", + "lang", + "non_public_metrics", + "note_tweet", + "organic_metrics", + "possibly_sensitive", + "promoted_metrics", + "public_metrics", + "referenced_tweets", + "reply_settings", + "scopes", + "source", + "text", + "withheld" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tweet fields to include in the response. Example: ['created_at','author_id','public_metrics']." + }, + "expansions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "article.cover_media", + "article.media_entities", + "attachments.media_keys", + "attachments.media_source_tweet", + "attachments.poll_ids", + "author_id", + "author_screen_name", + "edit_history_tweet_ids", + "entities.mentions.username", + "entities.note.mentions.username", + "geo.place_id", + "in_reply_to_user_id", + "referenced_tweets.id", + "referenced_tweets.id.author_id" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Expansions to hydrate related objects like users, media, polls, and places." + }, + "media_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "alt_text", + "duration_ms", + "height", + "media_key", + "non_public_metrics", + "organic_metrics", + "preview_image_url", + "promoted_metrics", + "public_metrics", + "type", + "url", + "variants", + "width" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Media fields to include when media keys are expanded." + }, + "place_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "contained_within", + "country", + "country_code", + "full_name", + "geo", + "id", + "name", + "place_type" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Place fields to include when place IDs are expanded." + }, + "poll_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "duration_minutes", + "end_datetime", + "id", + "options", + "voting_status" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "Poll fields to include when poll IDs are expanded." + }, + "user_fields": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "affiliation", + "connection_status", + "created_at", + "description", + "entities", + "id", + "location", + "most_recent_tweet_id", + "name", + "pinned_tweet_id", + "profile_banner_url", + "profile_image_url", + "protected", + "public_metrics", + "receives_your_dm", + "subscription_type", + "url", + "verified", + "verified_type", + "withheld", + "username" + ] + } + }, + { + "type": "null" + } + ], + "default": null, + "description": "User fields to include when user IDs are expanded. Username is always returned by default." + }, + "since_id": { + "type": "string", + "description": "Return Tweets more recent than this ID (cannot be used with start_time)." + }, + "until_id": { + "type": "string", + "description": "Return Tweets older than this ID (cannot be used with end_time)." + }, + "next_token": { + "type": "string", + "description": "Pagination token from a previous response's meta.next_token." + }, + "pagination_token": { + "type": "string", + "description": "Alternative pagination token from a previous meta.next_token; next_token is preferred." + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "mcpServerName": "twitter" + }, + "bash": { + "name": "executeCommand", + "type": "builtin" + } + } + }, + { + "name": "podcast_transcript_agent", + "model": "gpt-4.1", + "description": "An agent that will generate a transcript of a podcast", + "instructions": "You job is to create a NotebookLM style 1 minute podcast between 2 speakers John and Chloe. Each line should be a new speaker. The podcast should be about the contents of the two papers (that were selected). You can use [sighs], [inhales then exhales], [chuckles], [laughs], [clears throat], [coughs], [sniffs], [pauses] etc. to make the podcast more natural." + }, + { + "name": "elevenlabs_audio_gen", + "model": "gpt-4.1", + "description": "An agent that will generate an audio file from a text", + "instructions": "Your job is to take the mutli speaker transcript and generate an audio file from it. Use the elevenlabs text to speech tool to do this. For each speaker turn, you should generate an audio file and then combine them all into a single audio file. Use the voice_name 'Liam' for John and 'Cassidy' for Chloe. Make sure to remove the speaker names from the text before generating the audio files. Use the eleven_v3 model_id. In addition, you should use the compose_music tool to generate a short musical intro and outro for the podcast. The intro should be a small 5-10 second clip modeled after popular podcasts which fades and the podcast starts. The outro should be 10-15 seconds of a related sound. Save the intro and outro to files, and then use the bash tool to stitch them with the main podcast audio so that the final output audio file starts with the intro music, then the full conversation, and ends with the outro music. Place all generated audio on the Desktop by default unless otherwise instructed. Don't wait for confirmation - go ahead and produce the podcast.", + "tools": { + "text_to_speech": { + "name": "text_to_speech", + "type": "mcp", + "description": "Generate an audio file from a text", + "inputSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The text to generate an audio file from" + }, + "voice_name": { + "type": "string", + "description": "The voice name to use for the audio file" + }, + "model_id": { + "type": "string", + "description": "The model id to use for the audio file" + } + } + }, + "mcpServerName": "elevenLabs" + }, + "compose_music": { + "name": "compose_music", + "type": "mcp", + "description": "Generate intro and outro music for the podcast and save as audio files", + "inputSchema": { + "type": "object", + "properties": { + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Prompt" + }, + "output_directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Directory" + }, + "composition_plan": { + "anyOf": [ + { + "$ref": "#/$defs/MusicPrompt" + }, + { + "type": "null" + } + ], + "default": null + }, + "music_length_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Music Length Ms" + } + }, + "$defs": { + "MusicPrompt": { + "additionalProperties": true, + "properties": { + "positive_global_styles": { + "items": { + "type": "string" + }, + "title": "Positive Global Styles", + "type": "array" + }, + "negative_global_styles": { + "items": { + "type": "string" + }, + "title": "Negative Global Styles", + "type": "array" + }, + "sections": { + "items": { + "$ref": "#/$defs/SongSection" + }, + "title": "Sections", + "type": "array" + } + }, + "required": [ + "positive_global_styles", + "negative_global_styles", + "sections" + ], + "title": "MusicPrompt", + "type": "object" + }, + "SectionSource": { + "additionalProperties": true, + "properties": { + "song_id": { + "title": "Song Id", + "type": "string" + }, + "range": { + "$ref": "#/$defs/TimeRange" + }, + "negative_ranges": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TimeRange" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Negative Ranges" + } + }, + "required": [ + "song_id", + "range" + ], + "title": "SectionSource", + "type": "object" + }, + "SongSection": { + "additionalProperties": true, + "properties": { + "section_name": { + "title": "Section Name", + "type": "string" + }, + "positive_local_styles": { + "items": { + "type": "string" + }, + "title": "Positive Local Styles", + "type": "array" + }, + "negative_local_styles": { + "items": { + "type": "string" + }, + "title": "Negative Local Styles", + "type": "array" + }, + "duration_ms": { + "title": "Duration Ms", + "type": "integer" + }, + "lines": { + "items": { + "type": "string" + }, + "title": "Lines", + "type": "array" + }, + "source_from": { + "anyOf": [ + { + "$ref": "#/$defs/SectionSource" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "section_name", + "positive_local_styles", + "negative_local_styles", + "duration_ms", + "lines" + ], + "title": "SongSection", + "type": "object" + }, + "TimeRange": { + "additionalProperties": true, + "properties": { + "start_ms": { + "title": "Start Ms", + "type": "integer" + }, + "end_ms": { + "title": "End Ms", + "type": "integer" + } + }, + "required": [ + "start_ms", + "end_ms" + ], + "title": "TimeRange", + "type": "object" + } + }, + "title": "compose_musicArguments" + }, + "mcpServerName": "elevenLabs" + }, + "bash": { + "name": "executeCommand", + "type": "builtin" + } + } + } + ], + "mcpServers": { + "twitter": { + "type": "http", + "url": "" + }, + "elevenLabs": { + "command": "uvx", + "args": [ + "elevenlabs-mcp" + ], + "env": { + "ELEVENLABS_API_KEY": "sk_42ee2a0a19266552c18b0920b593e22f0185d4b1435b65ed" + } + } + } +} diff --git a/apps/cli/src/app.ts b/apps/cli/src/app.ts index e4d734ea..51673ebb 100644 --- a/apps/cli/src/app.ts +++ b/apps/cli/src/app.ts @@ -542,3 +542,81 @@ export async function importExample(exampleName?: string, filePath?: string) { export async function listExamples() { return listAvailableExamples(); } + +export async function exportWorkflow(entryAgentName: string) { + const agentsDir = path.join(WorkDir, "agents"); + const mcpConfigPath = path.join(WorkDir, "config", "mcp.json"); + + // Read MCP config + let mcpConfig: z.infer = { mcpServers: {} }; + try { + const mcpContent = await fsp.readFile(mcpConfigPath, "utf8"); + mcpConfig = McpServerConfig.parse(JSON.parse(mcpContent)); + } catch (error: any) { + if (error?.code !== "ENOENT") { + throw new Error(`Failed to read MCP config: ${error.message ?? error}`); + } + } + + // Recursively discover all agents and MCP servers + const discoveredAgents = new Map>(); + const discoveredMcpServers = new Set(); + + async function discoverAgent(agentName: string) { + if (discoveredAgents.has(agentName)) { + return; // Already processed + } + + // Load agent + const agentPath = path.join(agentsDir, `${agentName}.json`); + let agentContent: string; + try { + agentContent = await fsp.readFile(agentPath, "utf8"); + } catch (error: any) { + if (error?.code === "ENOENT") { + throw new Error(`Agent not found: ${agentName}`); + } + throw new Error(`Failed to read agent ${agentName}: ${error.message ?? error}`); + } + + const agent = Agent.parse(JSON.parse(agentContent)); + discoveredAgents.set(agentName, agent); + + // Process tools + if (agent.tools) { + for (const [toolKey, tool] of Object.entries(agent.tools)) { + if (tool.type === "agent") { + // Recursively discover dependent agent + await discoverAgent(tool.name); + } else if (tool.type === "mcp") { + // Track MCP server + discoveredMcpServers.add(tool.mcpServerName); + } + } + } + } + + // Start discovery from entry agent + await discoverAgent(entryAgentName); + + // Build MCP servers object + const workflowMcpServers: Record> = {}; + for (const serverName of discoveredMcpServers) { + if (mcpConfig.mcpServers[serverName]) { + workflowMcpServers[serverName] = mcpConfig.mcpServers[serverName]; + } else { + throw new Error(`MCP server '${serverName}' is referenced but not found in config`); + } + } + + // Build workflow object + const workflow: z.infer = { + id: entryAgentName, + entryAgent: entryAgentName, + agents: Array.from(discoveredAgents.values()), + ...(Object.keys(workflowMcpServers).length > 0 ? { mcpServers: workflowMcpServers } : {}), + }; + + // Output to stdout + console.log(JSON.stringify(workflow, null, 2)); +} From 61c2f8aa9d2e8ced32ec118d007507d3f834ab73 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 18:30:15 +0530 Subject: [PATCH 15/21] delete: remove misplaced podcast.json file --- apps/cli/podcast.json | 541 ------------------------------------------ 1 file changed, 541 deletions(-) delete mode 100644 apps/cli/podcast.json diff --git a/apps/cli/podcast.json b/apps/cli/podcast.json deleted file mode 100644 index 0321d90f..00000000 --- a/apps/cli/podcast.json +++ /dev/null @@ -1,541 +0,0 @@ -{ - "id": "tweet-podcast", - "instructions": "This example workflow generates a narrated podcast episode from recent AI-related tweets using multiple agents.", - "description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.", - "entryAgent": "tweet-podcast", - "agents": [ - { - "name": "tweet-podcast", - "model": "gpt-5.1", - "description": "An agent that will produce a podcast from recent tweets", - "instructions": "You are the orchestrator for producing a short podcast episode end-to-end. Follow these steps in order and only advance once each step succeeds:\n\n1. Tweets: call the tweets workflow to collect the latest tweets, .\n\n2.Transcript creation: Provide the resulting tweets to the podcast_transcript_agent tool so it can script a ~1 minute alternating dialogue between John and Chloe that references the tweets and a balanced conversation about AI bubble.\n\n4. Audio production: Send the transcript to the elevenlabs_audio_gen tool create an audio file.", - "tools": { - "tweets": { - "name": "tweets", - "type": "agent" - }, - "podcast_transcript_agent": { - "name": "podcast_transcript_agent", - "type": "agent" - }, - "elevenlabs_audio_gen": { - "name": "elevenlabs_audio_gen", - "type": "agent" - } - } - }, - { - "name": "tweets", - "model": "gpt-4.1", - "description": "Checks latest tweets", - "instructions": "Pulls the recent 10 recent tweets each on OpenAI, Anthropic, Nvidia, Grok, Gemini", - "tools": { - "search_tweets": { - "name": "TWITTER_RECENT_SEARCH", - "type": "mcp", - "description": "Search recent Tweets from the last 7 days using X/Twitter's search syntax via Composio's Twitter MCP server.", - "inputSchema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query for matching Tweets. Use X search operators like from:username, -is:retweet, -is:reply, has:media, lang:en, etc. Limited to last 7 days." - }, - "start_time": { - "type": "string", - "description": "Oldest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results, within the last 7 days." - }, - "end_time": { - "type": "string", - "description": "Newest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results; exclusive." - }, - "max_results": { - "type": "integer", - "description": "Number of Tweets to return (up to 2000 per call).", - "default": 10 - }, - "sort_order": { - "type": "string", - "enum": [ - "recency", - "relevancy" - ], - "description": "Order of results: 'recency' (most recent first) or 'relevancy'." - }, - "tweet_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "article", - "attachments", - "author_id", - "card_uri", - "context_annotations", - "conversation_id", - "created_at", - "edit_controls", - "edit_history_tweet_ids", - "entities", - "geo", - "id", - "in_reply_to_user_id", - "lang", - "non_public_metrics", - "note_tweet", - "organic_metrics", - "possibly_sensitive", - "promoted_metrics", - "public_metrics", - "referenced_tweets", - "reply_settings", - "scopes", - "source", - "text", - "withheld" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Tweet fields to include in the response. Example: ['created_at','author_id','public_metrics']." - }, - "expansions": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "article.cover_media", - "article.media_entities", - "attachments.media_keys", - "attachments.media_source_tweet", - "attachments.poll_ids", - "author_id", - "author_screen_name", - "edit_history_tweet_ids", - "entities.mentions.username", - "entities.note.mentions.username", - "geo.place_id", - "in_reply_to_user_id", - "referenced_tweets.id", - "referenced_tweets.id.author_id" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Expansions to hydrate related objects like users, media, polls, and places." - }, - "media_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "alt_text", - "duration_ms", - "height", - "media_key", - "non_public_metrics", - "organic_metrics", - "preview_image_url", - "promoted_metrics", - "public_metrics", - "type", - "url", - "variants", - "width" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Media fields to include when media keys are expanded." - }, - "place_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "contained_within", - "country", - "country_code", - "full_name", - "geo", - "id", - "name", - "place_type" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Place fields to include when place IDs are expanded." - }, - "poll_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "duration_minutes", - "end_datetime", - "id", - "options", - "voting_status" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "Poll fields to include when poll IDs are expanded." - }, - "user_fields": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "affiliation", - "connection_status", - "created_at", - "description", - "entities", - "id", - "location", - "most_recent_tweet_id", - "name", - "pinned_tweet_id", - "profile_banner_url", - "profile_image_url", - "protected", - "public_metrics", - "receives_your_dm", - "subscription_type", - "url", - "verified", - "verified_type", - "withheld", - "username" - ] - } - }, - { - "type": "null" - } - ], - "default": null, - "description": "User fields to include when user IDs are expanded. Username is always returned by default." - }, - "since_id": { - "type": "string", - "description": "Return Tweets more recent than this ID (cannot be used with start_time)." - }, - "until_id": { - "type": "string", - "description": "Return Tweets older than this ID (cannot be used with end_time)." - }, - "next_token": { - "type": "string", - "description": "Pagination token from a previous response's meta.next_token." - }, - "pagination_token": { - "type": "string", - "description": "Alternative pagination token from a previous meta.next_token; next_token is preferred." - } - }, - "required": [ - "query" - ], - "additionalProperties": false - }, - "mcpServerName": "twitter" - }, - "bash": { - "name": "executeCommand", - "type": "builtin" - } - } - }, - { - "name": "podcast_transcript_agent", - "model": "gpt-4.1", - "description": "An agent that will generate a transcript of a podcast", - "instructions": "You job is to create a NotebookLM style 1 minute podcast between 2 speakers John and Chloe. Each line should be a new speaker. The podcast should be about the contents of the two papers (that were selected). You can use [sighs], [inhales then exhales], [chuckles], [laughs], [clears throat], [coughs], [sniffs], [pauses] etc. to make the podcast more natural." - }, - { - "name": "elevenlabs_audio_gen", - "model": "gpt-4.1", - "description": "An agent that will generate an audio file from a text", - "instructions": "Your job is to take the mutli speaker transcript and generate an audio file from it. Use the elevenlabs text to speech tool to do this. For each speaker turn, you should generate an audio file and then combine them all into a single audio file. Use the voice_name 'Liam' for John and 'Cassidy' for Chloe. Make sure to remove the speaker names from the text before generating the audio files. Use the eleven_v3 model_id. In addition, you should use the compose_music tool to generate a short musical intro and outro for the podcast. The intro should be a small 5-10 second clip modeled after popular podcasts which fades and the podcast starts. The outro should be 10-15 seconds of a related sound. Save the intro and outro to files, and then use the bash tool to stitch them with the main podcast audio so that the final output audio file starts with the intro music, then the full conversation, and ends with the outro music. Place all generated audio on the Desktop by default unless otherwise instructed. Don't wait for confirmation - go ahead and produce the podcast.", - "tools": { - "text_to_speech": { - "name": "text_to_speech", - "type": "mcp", - "description": "Generate an audio file from a text", - "inputSchema": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "The text to generate an audio file from" - }, - "voice_name": { - "type": "string", - "description": "The voice name to use for the audio file" - }, - "model_id": { - "type": "string", - "description": "The model id to use for the audio file" - } - } - }, - "mcpServerName": "elevenLabs" - }, - "compose_music": { - "name": "compose_music", - "type": "mcp", - "description": "Generate intro and outro music for the podcast and save as audio files", - "inputSchema": { - "type": "object", - "properties": { - "prompt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Prompt" - }, - "output_directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Output Directory" - }, - "composition_plan": { - "anyOf": [ - { - "$ref": "#/$defs/MusicPrompt" - }, - { - "type": "null" - } - ], - "default": null - }, - "music_length_ms": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Music Length Ms" - } - }, - "$defs": { - "MusicPrompt": { - "additionalProperties": true, - "properties": { - "positive_global_styles": { - "items": { - "type": "string" - }, - "title": "Positive Global Styles", - "type": "array" - }, - "negative_global_styles": { - "items": { - "type": "string" - }, - "title": "Negative Global Styles", - "type": "array" - }, - "sections": { - "items": { - "$ref": "#/$defs/SongSection" - }, - "title": "Sections", - "type": "array" - } - }, - "required": [ - "positive_global_styles", - "negative_global_styles", - "sections" - ], - "title": "MusicPrompt", - "type": "object" - }, - "SectionSource": { - "additionalProperties": true, - "properties": { - "song_id": { - "title": "Song Id", - "type": "string" - }, - "range": { - "$ref": "#/$defs/TimeRange" - }, - "negative_ranges": { - "anyOf": [ - { - "items": { - "$ref": "#/$defs/TimeRange" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Negative Ranges" - } - }, - "required": [ - "song_id", - "range" - ], - "title": "SectionSource", - "type": "object" - }, - "SongSection": { - "additionalProperties": true, - "properties": { - "section_name": { - "title": "Section Name", - "type": "string" - }, - "positive_local_styles": { - "items": { - "type": "string" - }, - "title": "Positive Local Styles", - "type": "array" - }, - "negative_local_styles": { - "items": { - "type": "string" - }, - "title": "Negative Local Styles", - "type": "array" - }, - "duration_ms": { - "title": "Duration Ms", - "type": "integer" - }, - "lines": { - "items": { - "type": "string" - }, - "title": "Lines", - "type": "array" - }, - "source_from": { - "anyOf": [ - { - "$ref": "#/$defs/SectionSource" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "required": [ - "section_name", - "positive_local_styles", - "negative_local_styles", - "duration_ms", - "lines" - ], - "title": "SongSection", - "type": "object" - }, - "TimeRange": { - "additionalProperties": true, - "properties": { - "start_ms": { - "title": "Start Ms", - "type": "integer" - }, - "end_ms": { - "title": "End Ms", - "type": "integer" - } - }, - "required": [ - "start_ms", - "end_ms" - ], - "title": "TimeRange", - "type": "object" - } - }, - "title": "compose_musicArguments" - }, - "mcpServerName": "elevenLabs" - }, - "bash": { - "name": "executeCommand", - "type": "builtin" - } - } - } - ], - "mcpServers": { - "twitter": { - "type": "http", - "url": "" - }, - "elevenLabs": { - "command": "uvx", - "args": [ - "elevenlabs-mcp" - ], - "env": { - "ELEVENLABS_API_KEY": "sk_42ee2a0a19266552c18b0920b593e22f0185d4b1435b65ed" - } - } - } -} From 5a8bc0b1a0252e5e3e70e44c73e8aa524deb9851 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 19:23:53 +0530 Subject: [PATCH 16/21] removed incomplete gemini3-test example json --- apps/cli/src/examples/gemini3-test.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 apps/cli/src/examples/gemini3-test.json diff --git a/apps/cli/src/examples/gemini3-test.json b/apps/cli/src/examples/gemini3-test.json deleted file mode 100644 index 54f63c38..00000000 --- a/apps/cli/src/examples/gemini3-test.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "gemini3_svg_pelican", - "provider": "google", - "model": "gemini-3.0-pro", - "description": "Outputs a single valid SVG depicting a pelican riding a bicycle.", - "instructions": "You must output only a single, valid, self-contained SVG XML depicting a pelican riding a bicycle. Requirements: 1) Output must be ONLY raw SVG XML (no markdown fences, no explanations). 2) Use viewBox=\"0 0 512 512\" and set width/height to 512. 3) Include clear, recognizable pelican and bicycle using basic shapes/paths. 4) No external refs, images, scripts, or styles; use inline attributes only. 5) Keep IDs minimal; keep total file size reasonable.", - "tools": {} -} \ No newline at end of file From e3bc26d2e6e964cda299a9d66f52ba179f33357b Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Fri, 21 Nov 2025 19:24:36 +0530 Subject: [PATCH 17/21] remove: eliminate gemini3-test example from exports --- apps/cli/src/examples/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/cli/src/examples/index.ts b/apps/cli/src/examples/index.ts index 83abf58e..428356d2 100644 --- a/apps/cli/src/examples/index.ts +++ b/apps/cli/src/examples/index.ts @@ -1,9 +1,7 @@ import twitterPodcast from './twitter-podcast.json' with { type: 'json' }; -import gemini3Test from './gemini3-test.json' with { type: 'json' }; import { Example } from '../application/entities/example.js'; import z from 'zod'; export const examples: Record> = { "twitter-podcast": Example.parse(twitterPodcast), - "gemini3-test": Example.parse(gemini3Test), }; \ No newline at end of file From 83c54c1c93863347a29de35e1ef4f86c10502f11 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Mon, 24 Nov 2025 10:44:05 +0530 Subject: [PATCH 18/21] Fix: better prompting around MCP config Add: copilot tool to add MCP servers --- .../src/application/assistant/instructions.ts | 7 +- .../assistant/skills/mcp-integration/skill.ts | 244 +++++++++++++++++- .../skills/workflow-authoring/skill.ts | 208 ++++++++++++++- apps/cli/src/application/lib/builtin-tools.ts | 113 ++++++++ 4 files changed, 552 insertions(+), 20 deletions(-) diff --git a/apps/cli/src/application/assistant/instructions.ts b/apps/cli/src/application/assistant/instructions.ts index aca6a8a5..8321c6b0 100644 --- a/apps/cli/src/application/assistant/instructions.ts +++ b/apps/cli/src/application/assistant/instructions.ts @@ -38,11 +38,16 @@ Always consult this catalog first so you load the right skills before taking act - \`deleteFile\`, \`createFile\`, \`updateFile\`, \`readFile\` - File operations - \`listFiles\`, \`exploreDirectory\` - Directory exploration - \`analyzeAgent\` - Agent analysis -- \`listMcpServers\`, \`listMcpTools\` - MCP server management +- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\` - MCP server management - \`loadSkill\` - Skill loading These tools work directly and are NOT filtered by \`.rowboat/config/security.json\`. +**CRITICAL: MCP Server Configuration** +- ALWAYS use the \`addMcpServer\` builtin tool to add or update MCP servers—it validates the configuration before saving +- NEVER manually edit \`config/mcp.json\` using \`createFile\` or \`updateFile\` for MCP servers +- Invalid MCP configs will prevent the agent from starting with validation errors + **Only \`executeCommand\` (shell/bash commands) is filtered** by the security allowlist. If you need to delete a file, use the \`deleteFile\` builtin tool, not \`executeCommand\` with \`rm\`. If you need to create a file, use \`createFile\`, not \`executeCommand\` with \`touch\` or \`echo >\`. The security allowlist in \`security.json\` only applies to shell commands executed via \`executeCommand\`, not to Rowboat's internal builtin tools. diff --git a/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts b/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts index 1f3aa313..f8aa0846 100644 --- a/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts +++ b/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts @@ -1,22 +1,225 @@ export const skill = String.raw` # MCP Integration Guidance -Load this skill whenever a user asks about external tools, MCP servers, or how to extend an agent’s capabilities. +Load this skill whenever a user asks about external tools, MCP servers, or how to extend an agent's capabilities. ## Key concepts - MCP servers expose tools (web scraping, APIs, databases, etc.) declared in \`config/mcp.json\`. - Agents reference MCP tools through the \`"tools"\` block by specifying \`type\`, \`name\`, \`description\`, \`mcpServerName\`, and a full \`inputSchema\`. - Tool schemas can include optional property descriptions; only include \`"required"\` when parameters are mandatory. +## CRITICAL: Adding MCP Servers + +**ALWAYS use the \`addMcpServer\` builtin tool** to add or update MCP server configurations. This tool validates the configuration before saving and prevents startup errors. + +**NEVER manually create or edit \`config/mcp.json\`** using \`createFile\` or \`updateFile\` for MCP servers—this bypasses validation and will cause errors. + +### MCP Server Configuration Schema + +There are TWO types of MCP servers: + +#### 1. STDIO (Command-based) Servers +For servers that run as local processes (Node.js, Python, etc.): + +**Required fields:** +- \`command\`: string (e.g., "npx", "node", "python", "uvx") + +**Optional fields:** +- \`args\`: array of strings (command arguments) +- \`env\`: object with string key-value pairs (environment variables) +- \`type\`: "stdio" (optional, inferred from presence of \`command\`) + +**Schema:** +\`\`\`json +{ + "type": "stdio", + "command": "string (REQUIRED)", + "args": ["string", "..."], + "env": { + "KEY": "value" + } +} +\`\`\` + +**Valid STDIO examples:** +\`\`\`json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/data"] +} +\`\`\` + +\`\`\`json +{ + "command": "python", + "args": ["-m", "mcp_server_git"], + "env": { + "GIT_REPO_PATH": "/path/to/repo" + } +} +\`\`\` + +\`\`\`json +{ + "command": "uvx", + "args": ["mcp-server-fetch"] +} +\`\`\` + +#### 2. HTTP/SSE Servers +For servers that expose HTTP or Server-Sent Events endpoints: + +**Required fields:** +- \`url\`: string (complete URL including protocol and path) + +**Optional fields:** +- \`headers\`: object with string key-value pairs (HTTP headers) +- \`type\`: "http" (optional, inferred from presence of \`url\`) + +**Schema:** +\`\`\`json +{ + "type": "http", + "url": "string (REQUIRED)", + "headers": { + "Authorization": "Bearer token", + "Custom-Header": "value" + } +} +\`\`\` + +**Valid HTTP examples:** +\`\`\`json +{ + "url": "http://localhost:3000/sse" +} +\`\`\` + +\`\`\`json +{ + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer sk-1234567890" + } +} +\`\`\` + +### Common Validation Errors to Avoid + +❌ **WRONG - Missing required field:** +\`\`\`json +{ + "args": ["some-arg"] +} +\`\`\` +Error: Missing \`command\` for stdio OR \`url\` for http + +❌ **WRONG - Empty object:** +\`\`\`json +{} +\`\`\` +Error: Must have either \`command\` (stdio) or \`url\` (http) + +❌ **WRONG - Mixed types:** +\`\`\`json +{ + "command": "npx", + "url": "http://localhost:3000" +} +\`\`\` +Error: Cannot have both \`command\` and \`url\` + +✅ **CORRECT - Minimal stdio:** +\`\`\`json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-time"] +} +\`\`\` + +✅ **CORRECT - Minimal http:** +\`\`\`json +{ + "url": "http://localhost:3000/sse" +} +\`\`\` + +### Using addMcpServer Tool + +**Example 1: Add stdio server** +\`\`\`json +{ + "serverName": "filesystem", + "serverType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/data"] +} +\`\`\` + +**Example 2: Add HTTP server** +\`\`\`json +{ + "serverName": "custom-api", + "serverType": "http", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token123" + } +} +\`\`\` + +**Example 3: Add Python MCP server** +\`\`\`json +{ + "serverName": "github", + "serverType": "stdio", + "command": "python", + "args": ["-m", "mcp_server_github"], + "env": { + "GITHUB_TOKEN": "ghp_xxxxx" + } +} +\`\`\` + ## Operator actions 1. Use \`listMcpServers\` to enumerate configured servers. -2. Use \`listMcpTools\` for a server to understand the available operations and schemas. -3. Explain which MCP tools match the user’s needs before editing agent definitions. -4. When adding a tool to an agent, document what it does and ensure the schema mirrors the MCP definition. +2. Use \`addMcpServer\` to add or update MCP server configurations (with validation). +3. Use \`listMcpTools\` for a server to understand the available operations and schemas. +4. Explain which MCP tools match the user's needs before editing agent definitions. +5. When adding a tool to an agent, document what it does and ensure the schema mirrors the MCP definition. -## Example snippets to reference -- Firecrawl search (required param): +## Adding MCP Tools to Agents + +Once an MCP server is configured, add its tools to agent definitions: + +### MCP Tool Format in Agent +\`\`\`json +"tools": { + "descriptive_key": { + "type": "mcp", + "name": "actual_tool_name_from_server", + "description": "What the tool does", + "mcpServerName": "server_name_from_config", + "inputSchema": { + "type": "object", + "properties": { + "param1": {"type": "string", "description": "What param1 means"} + }, + "required": ["param1"] + } + } +} \`\`\` + +### Tool Schema Rules +- Use \`listMcpTools\` to get the exact \`inputSchema\` from the server +- Copy the schema exactly as provided by the MCP server +- Only include \`"required"\` array if parameters are truly mandatory +- Add descriptions to help the agent understand parameter usage + +### Example snippets to reference +- Firecrawl search (required param): +\`\`\`json "tools": { "search": { "type": "mcp", @@ -34,8 +237,9 @@ Load this skill whenever a user asks about external tools, MCP servers, or how t } } \`\`\` + - ElevenLabs text-to-speech (no required array): -\`\`\` +\`\`\`json "tools": { "text_to_speech": { "type": "mcp", @@ -52,9 +256,31 @@ Load this skill whenever a user asks about external tools, MCP servers, or how t } \`\`\` +- Filesystem operations: +\`\`\`json +"tools": { + "read_file": { + "type": "mcp", + "name": "read_file", + "description": "Read file contents", + "mcpServerName": "filesystem", + "inputSchema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "File path to read"} + }, + "required": ["path"] + } + } +} +\`\`\` + ## Safety reminders -- Only recommend MCP tools that are actually configured. -- Clarify any missing details (required parameters, server names) before modifying files. +- ALWAYS use \`addMcpServer\` to configure MCP servers—never manually edit config files +- Only recommend MCP tools that are actually configured (use \`listMcpServers\` first) +- Clarify any missing details (required parameters, server names) before modifying files +- Test server connection with \`listMcpTools\` after adding a new server +- Invalid MCP configs prevent agents from starting—validation is critical `; export default skill; diff --git a/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts b/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts index dd6dfc0e..098b39ae 100644 --- a/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts +++ b/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts @@ -19,7 +19,45 @@ Load this skill whenever a user wants to inspect, create, or update agents insid 3. The orchestrator calls other agents as tools when needed 4. Data flows through tool call parameters and responses -## Agent format +## Agent File Schema + +Agent files MUST conform to this exact schema. Invalid agents will fail to load. + +### Complete Agent Schema +\`\`\`json +{ + "name": "string (REQUIRED, must match filename without .json)", + "description": "string (REQUIRED, what this agent does)", + "instructions": "string (REQUIRED, detailed instructions for the agent)", + "model": "string (OPTIONAL, e.g., 'gpt-5.1', 'claude-sonnet-4-5')", + "provider": "string (OPTIONAL, provider alias from models.json)", + "tools": { + "descriptive_key": { + "type": "builtin | mcp | agent (REQUIRED)", + "name": "string (REQUIRED)", + // Additional fields depend on type - see below + } + } +} +\`\`\` + +### Required Fields +- \`name\`: Agent identifier (must exactly match the filename without .json) +- \`description\`: Brief description of agent's purpose +- \`instructions\`: Detailed instructions for how the agent should behave + +### Optional Fields +- \`model\`: Model to use (defaults to model config if not specified) +- \`provider\`: Provider alias from models.json (optional) +- \`tools\`: Object containing tool definitions (can be empty or omitted) + +### Naming Rules +- Agent filename MUST match the \`name\` field exactly +- Example: If \`name\` is "summariser_agent", file must be "summariser_agent.json" +- Use lowercase with underscores for multi-word names +- No spaces or special characters in names + +### Agent Format Example \`\`\`json { "name": "agent_name", @@ -43,9 +81,26 @@ Load this skill whenever a user wants to inspect, create, or update agents insid } \`\`\` -## Tool types +## Tool Types & Schemas -### Builtin tools +Tools in agents must follow one of three types. Each has specific required fields. + +### 1. Builtin Tools +Internal Rowboat tools (executeCommand, file operations, MCP queries, etc.) + +**Schema:** +\`\`\`json +{ + "type": "builtin", + "name": "tool_name" +} +\`\`\` + +**Required fields:** +- \`type\`: Must be "builtin" +- \`name\`: Builtin tool name (e.g., "executeCommand", "readFile") + +**Example:** \`\`\`json "bash": { "type": "builtin", @@ -53,7 +108,42 @@ Load this skill whenever a user wants to inspect, create, or update agents insid } \`\`\` -### MCP tools +**Available builtin tools:** +- \`executeCommand\` - Execute shell commands +- \`readFile\`, \`createFile\`, \`updateFile\`, \`deleteFile\` - File operations +- \`listFiles\`, \`exploreDirectory\` - Directory operations +- \`analyzeAgent\` - Analyze agent structure +- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\` - MCP management +- \`loadSkill\` - Load skill guidance + +### 2. MCP Tools +Tools from external MCP servers (APIs, databases, web scraping, etc.) + +**Schema:** +\`\`\`json +{ + "type": "mcp", + "name": "tool_name_from_server", + "description": "What the tool does", + "mcpServerName": "server_name_from_config", + "inputSchema": { + "type": "object", + "properties": { + "param": {"type": "string", "description": "Parameter description"} + }, + "required": ["param"] + } +} +\`\`\` + +**Required fields:** +- \`type\`: Must be "mcp" +- \`name\`: Exact tool name from MCP server +- \`description\`: What the tool does (helps agent understand when to use it) +- \`mcpServerName\`: Server name from config/mcp.json +- \`inputSchema\`: Full JSON Schema object for tool parameters + +**Example:** \`\`\`json "search": { "type": "mcp", @@ -70,17 +160,40 @@ Load this skill whenever a user wants to inspect, create, or update agents insid } \`\`\` -### Agent tools (for chaining agents) +**Important:** +- Use \`listMcpTools\` to get the exact inputSchema from the server +- Copy the schema exactly—don't modify property types or structure +- Only include \`"required"\` array if parameters are mandatory + +### 3. Agent Tools (for chaining agents) +Reference other agents as tools to build multi-agent workflows + +**Schema:** +\`\`\`json +{ + "type": "agent", + "name": "target_agent_name" +} +\`\`\` + +**Required fields:** +- \`type\`: Must be "agent" +- \`name\`: Name of the target agent (must exist in agents/ directory) + +**Example:** \`\`\`json "summariser": { "type": "agent", "name": "summariser_agent" } \`\`\` + +**How it works:** - Use \`"type": "agent"\` to call other agents as tools - The target agent will be invoked with the parameters you pass - Results are returned as tool output - This is how you build multi-agent workflows +- The referenced agent file must exist (e.g., agents/summariser_agent.json) ## Complete Multi-Agent Workflow Example @@ -156,13 +269,88 @@ Load this skill whenever a user wants to inspect, create, or update agents insid 5. **Tool naming**: Use descriptive tool keys (e.g., "summariser", "fetch_data", "analyze") 6. **Orchestration**: Create a top-level agent that coordinates the workflow +## Validation & Best Practices + +### CRITICAL: Schema Compliance +- Agent files MUST have \`name\`, \`description\`, and \`instructions\` fields +- Agent filename MUST exactly match the \`name\` field +- Tools MUST have valid \`type\` ("builtin", "mcp", or "agent") +- MCP tools MUST have all required fields: name, description, mcpServerName, inputSchema +- Agent tools MUST reference existing agent files +- Invalid agents will fail to load and prevent workflow execution + +### File Creation/Update Process +1. When creating an agent, use \`createFile\` with complete, valid JSON +2. When updating an agent, read it first with \`readFile\`, modify, then use \`updateFile\` +3. Validate JSON syntax before writing—malformed JSON breaks the agent +4. Test agent loading after creation/update by using \`analyzeAgent\` + +### Common Validation Errors to Avoid + +❌ **WRONG - Missing required fields:** +\`\`\`json +{ + "name": "my_agent" + // Missing description and instructions +} +\`\`\` + +❌ **WRONG - Filename mismatch:** +- File: agents/my_agent.json +- Content: {"name": "myagent", ...} + +❌ **WRONG - Invalid tool type:** +\`\`\`json +"tool1": { + "type": "custom", // Invalid type + "name": "something" +} +\`\`\` + +❌ **WRONG - MCP tool missing required fields:** +\`\`\`json +"search": { + "type": "mcp", + "name": "firecrawl_search" + // Missing: description, mcpServerName, inputSchema +} +\`\`\` + +✅ **CORRECT - Minimal valid agent:** +\`\`\`json +{ + "name": "simple_agent", + "description": "A simple agent", + "instructions": "Do simple tasks" +} +\`\`\` + +✅ **CORRECT - Complete MCP tool:** +\`\`\`json +"search": { + "type": "mcp", + "name": "firecrawl_search", + "description": "Search the web", + "mcpServerName": "firecrawl", + "inputSchema": { + "type": "object", + "properties": { + "query": {"type": "string"} + } + } +} +\`\`\` + ## Capabilities checklist 1. Explore \`agents/\` directory to understand existing agents before editing -2. Update files carefully to maintain schema validity -3. When creating multi-agent workflows, create an orchestrator agent -4. Add other agents as tools with \`"type": "agent"\` for chaining -5. List and explore MCP servers/tools when users need new capabilities -6. Confirm work done and outline next steps once changes are complete +2. Read existing agents with \`readFile\` before making changes +3. Validate all required fields are present before creating/updating agents +4. Ensure filename matches the \`name\` field exactly +5. Use \`analyzeAgent\` to verify agent structure after creation/update +6. When creating multi-agent workflows, create an orchestrator agent +7. Add other agents as tools with \`"type": "agent"\` for chaining +8. Use \`listMcpServers\` and \`listMcpTools\` when adding MCP integrations +9. Confirm work done and outline next steps once changes are complete `; export default skill; diff --git a/apps/cli/src/application/lib/builtin-tools.ts b/apps/cli/src/application/lib/builtin-tools.ts index 5c4ae64b..80158d77 100644 --- a/apps/cli/src/application/lib/builtin-tools.ts +++ b/apps/cli/src/application/lib/builtin-tools.ts @@ -8,6 +8,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { Client } from "@modelcontextprotocol/sdk/client"; import { resolveSkill, availableSkills } from "../assistant/skills/index.js"; +import { McpServerDefinition, McpServerConfig } from "../entities/mcp.js"; const BuiltinToolsSchema = z.record(z.string(), z.object({ description: z.string(), @@ -305,6 +306,118 @@ export const BuiltinTools: z.infer = { }, }, + addMcpServer: { + description: 'Add or update an MCP server in the configuration with validation. This ensures the server definition is valid before saving.', + inputSchema: z.object({ + serverName: z.string().describe('Name/alias for the MCP server'), + serverType: z.enum(['stdio', 'http']).describe('Type of MCP server: "stdio" for command-based or "http" for HTTP/SSE-based'), + command: z.string().optional().describe('Command to execute (required for stdio type, e.g., "npx", "python", "node")'), + args: z.array(z.string()).optional().describe('Command arguments (optional, for stdio type)'), + env: z.record(z.string(), z.string()).optional().describe('Environment variables (optional, for stdio type)'), + url: z.string().optional().describe('HTTP/SSE endpoint URL (required for http type)'), + headers: z.record(z.string(), z.string()).optional().describe('HTTP headers (optional, for http type)'), + }), + execute: async ({ serverName, serverType, command, args, env, url, headers }: { + serverName: string; + serverType: 'stdio' | 'http'; + command?: string; + args?: string[]; + env?: Record; + url?: string; + headers?: Record; + }) => { + try { + // Build server definition based on type + let serverDef: any; + if (serverType === 'stdio') { + if (!command) { + return { + success: false, + message: 'For stdio type servers, "command" is required. Example: "npx" or "python"', + validationErrors: ['Missing required field: command'], + }; + } + serverDef = { + type: 'stdio', + command, + ...(args ? { args } : {}), + ...(env ? { env } : {}), + }; + } else if (serverType === 'http') { + if (!url) { + return { + success: false, + message: 'For http type servers, "url" is required. Example: "http://localhost:3000/sse"', + validationErrors: ['Missing required field: url'], + }; + } + serverDef = { + type: 'http', + url, + ...(headers ? { headers } : {}), + }; + } else { + return { + success: false, + message: `Invalid serverType: ${serverType}. Must be "stdio" or "http"`, + validationErrors: [`Invalid serverType: ${serverType}`], + }; + } + + // Validate against Zod schema + const validationResult = McpServerDefinition.safeParse(serverDef); + if (!validationResult.success) { + return { + success: false, + message: 'Server definition failed validation. Check the errors below.', + validationErrors: validationResult.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`), + providedDefinition: serverDef, + }; + } + + // Read existing config + const configPath = path.join(BASE_DIR, 'config', 'mcp.json'); + let currentConfig: z.infer = { mcpServers: {} }; + try { + const content = await fs.readFile(configPath, 'utf-8'); + currentConfig = McpServerConfig.parse(JSON.parse(content)); + } catch (error: any) { + if (error?.code !== 'ENOENT') { + return { + success: false, + message: `Failed to read existing MCP config: ${error.message}`, + }; + } + // File doesn't exist, use empty config + } + + // Check if server already exists + const isUpdate = !!currentConfig.mcpServers[serverName]; + + // Add/update server + currentConfig.mcpServers[serverName] = validationResult.data; + + // Write back to file + await fs.mkdir(path.dirname(configPath), { recursive: true }); + await fs.writeFile(configPath, JSON.stringify(currentConfig, null, 2), 'utf-8'); + + return { + success: true, + message: `MCP server '${serverName}' ${isUpdate ? 'updated' : 'added'} successfully`, + serverName, + serverType, + isUpdate, + configuration: validationResult.data, + }; + } catch (error) { + return { + success: false, + message: `Failed to add MCP server: ${error instanceof Error ? error.message : 'Unknown error'}`, + }; + } + }, + }, + listMcpServers: { description: 'List all available MCP servers from the configuration', inputSchema: z.object({}), From d0eb8d6b3ff3c3f97b8cba6164042fa9b095ebed Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Tue, 25 Nov 2025 11:46:54 +0530 Subject: [PATCH 19/21] clean up prompt --- .../assistant/skills/mcp-integration/skill.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts b/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts index f8aa0846..93cf03db 100644 --- a/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts +++ b/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts @@ -256,24 +256,6 @@ Once an MCP server is configured, add its tools to agent definitions: } \`\`\` -- Filesystem operations: -\`\`\`json -"tools": { - "read_file": { - "type": "mcp", - "name": "read_file", - "description": "Read file contents", - "mcpServerName": "filesystem", - "inputSchema": { - "type": "object", - "properties": { - "path": {"type": "string", "description": "File path to read"} - }, - "required": ["path"] - } - } -} -\`\`\` ## Safety reminders - ALWAYS use \`addMcpServer\` to configure MCP servers—never manually edit config files From 294f166a3c46d1ebabdb37221bb283aa591fabeb Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Tue, 2 Dec 2025 10:34:11 +0530 Subject: [PATCH 20/21] Feat: add MCP execute tool to copilot and prompting around using it --- .../src/application/assistant/instructions.ts | 10 +- .../assistant/skills/builtin-tools/skill.ts | 31 ++++ .../src/application/assistant/skills/index.ts | 2 +- .../assistant/skills/mcp-integration/skill.ts | 175 +++++++++++++++++- apps/cli/src/application/lib/builtin-tools.ts | 116 ++++++++++++ 5 files changed, 329 insertions(+), 5 deletions(-) diff --git a/apps/cli/src/application/assistant/instructions.ts b/apps/cli/src/application/assistant/instructions.ts index 8321c6b0..a479025e 100644 --- a/apps/cli/src/application/assistant/instructions.ts +++ b/apps/cli/src/application/assistant/instructions.ts @@ -22,6 +22,14 @@ Always consult this catalog first so you load the right skills before taking act - Summarize completed work and suggest logical next steps at the end of a task. - Always ask for confirmation before taking destructive actions. +## MCP Tool Discovery (CRITICAL) + +**ALWAYS check for MCP tools BEFORE saying you can't do something.** + +When a user asks for ANY task that might require external capabilities (web search, internet access, APIs, data fetching, etc.), check MCP tools first using \`listMcpServers\` and \`listMcpTools\`. Load the "mcp-integration" skill for detailed guidance on discovering and executing MCP tools. + +**DO NOT** immediately respond with "I can't access the internet" or "I don't have that capability" without checking MCP tools first! + ## Execution reminders - Explore existing files and structure before creating new assets. - Use relative paths (no \${BASE_DIR} prefixes) when running commands or referencing files. @@ -38,7 +46,7 @@ Always consult this catalog first so you load the right skills before taking act - \`deleteFile\`, \`createFile\`, \`updateFile\`, \`readFile\` - File operations - \`listFiles\`, \`exploreDirectory\` - Directory exploration - \`analyzeAgent\` - Agent analysis -- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\` - MCP server management +- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\`, \`executeMcpTool\` - MCP server management and execution - \`loadSkill\` - Skill loading These tools work directly and are NOT filtered by \`.rowboat/config/security.json\`. diff --git a/apps/cli/src/application/assistant/skills/builtin-tools/skill.ts b/apps/cli/src/application/assistant/skills/builtin-tools/skill.ts index 2467367b..492d017e 100644 --- a/apps/cli/src/application/assistant/skills/builtin-tools/skill.ts +++ b/apps/cli/src/application/assistant/skills/builtin-tools/skill.ts @@ -150,6 +150,37 @@ Agents can call other agents as tools to create complex multi-step workflows. Th While \`executeCommand\` is the most versatile, other builtin tools exist for specific Rowboat operations (file management, agent inspection, etc.). These are primarily used by the Rowboat copilot itself and are not typically needed in user agents. If you need file operations, consider using bash commands like \`cat\`, \`echo\`, \`tee\`, etc. through \`executeCommand\`. +### Copilot-Specific Builtin Tools + +The Rowboat copilot has access to special builtin tools that regular agents don't typically use. These tools help the copilot assist users with workspace management and MCP integration: + +#### File & Directory Operations +- \`exploreDirectory\` - Recursively explore directory structure +- \`readFile\` - Read and parse file contents +- \`createFile\` - Create a new file with content +- \`updateFile\` - Update or overwrite existing file contents +- \`deleteFile\` - Delete a file +- \`listFiles\` - List all files and directories + +#### Agent Operations +- \`analyzeAgent\` - Read and analyze an agent file structure +- \`loadSkill\` - Load a Rowboat skill definition into context + +#### MCP Operations +- \`addMcpServer\` - Add or update an MCP server configuration (with validation) +- \`listMcpServers\` - List all available MCP servers +- \`listMcpTools\` - List all available tools from a specific MCP server +- \`executeMcpTool\` - **Execute a specific MCP tool on behalf of the user** + +#### Using executeMcpTool as Copilot + +The \`executeMcpTool\` builtin allows the copilot to directly execute MCP tools without creating an agent. Load the "mcp-integration" skill for complete guidance on discovering and executing MCP tools, including workflows, schema matching, and examples. + +**When to use executeMcpTool vs creating an agent:** +- Use \`executeMcpTool\` for immediate, one-time tasks +- Create an agent when the user needs repeated use or autonomous operation +- Create an agent for complex multi-step workflows involving multiple tools + ## Best Practices 1. **Give agents clear examples** in their instructions showing exact bash commands to run diff --git a/apps/cli/src/application/assistant/skills/index.ts b/apps/cli/src/application/assistant/skills/index.ts index 3d0f5fc5..e8c4f808 100644 --- a/apps/cli/src/application/assistant/skills/index.ts +++ b/apps/cli/src/application/assistant/skills/index.ts @@ -43,7 +43,7 @@ const definitions: SkillDefinition[] = [ id: "mcp-integration", title: "MCP Integration Guidance", folder: "mcp-integration", - summary: "Listing MCP servers/tools and embedding their schemas in agent definitions.", + summary: "Discovering, executing, and integrating MCP tools. Use this to check what external capabilities are available and execute MCP tools on behalf of users.", content: mcpIntegrationSkill, }, { diff --git a/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts b/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts index 93cf03db..ffbe7a21 100644 --- a/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts +++ b/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts @@ -1,7 +1,31 @@ export const skill = String.raw` # MCP Integration Guidance -Load this skill whenever a user asks about external tools, MCP servers, or how to extend an agent's capabilities. +**Load this skill proactively** when a user asks for ANY task that might require external capabilities (web search, internet access, APIs, data fetching, time/date, etc.). This skill provides complete guidance on discovering and executing MCP tools. + +## CRITICAL: Always Check MCP Tools First + +**IMPORTANT**: When a user asks for ANY task that might require external capabilities (web search, API calls, data fetching, etc.), ALWAYS: + +1. **First check**: Call \`listMcpServers\` to see what's available +2. **Then list tools**: Call \`listMcpTools\` on relevant servers +3. **Execute if possible**: Use \`executeMcpTool\` if a tool matches the need +4. **Only then decline**: If no MCP tool can help, explain what's not possible + +**DO NOT** immediately say "I can't do that" or "I don't have internet access" without checking MCP tools first! + +### Common User Requests and MCP Tools + +| User Request | Check For | Likely Tool | +|--------------|-----------|-------------| +| "Search the web/internet" | firecrawl, composio, fetch | \`firecrawl_search\`, \`COMPOSIO_SEARCH_WEB\` | +| "Scrape this website" | firecrawl | \`firecrawl_scrape\` | +| "Read/write files" | filesystem | \`read_file\`, \`write_file\` | +| "Get current time/date" | time | \`get_current_time\` | +| "Make HTTP request" | fetch | \`fetch\`, \`post\` | +| "GitHub operations" | github | \`create_issue\`, \`search_repos\` | +| "Generate audio/speech" | elevenLabs | \`text_to_speech\` | +| "Tweet/social media" | twitter, composio | Various social tools | ## Key concepts - MCP servers expose tools (web scraping, APIs, databases, etc.) declared in \`config/mcp.json\`. @@ -185,8 +209,153 @@ Error: Cannot have both \`command\` and \`url\` 1. Use \`listMcpServers\` to enumerate configured servers. 2. Use \`addMcpServer\` to add or update MCP server configurations (with validation). 3. Use \`listMcpTools\` for a server to understand the available operations and schemas. -4. Explain which MCP tools match the user's needs before editing agent definitions. -5. When adding a tool to an agent, document what it does and ensure the schema mirrors the MCP definition. +4. Use \`executeMcpTool\` to run MCP tools directly on behalf of the user. +5. Explain which MCP tools match the user's needs before editing agent definitions. +6. When adding a tool to an agent, document what it does and ensure the schema mirrors the MCP definition. + +## Executing MCP Tools Directly (Copilot) + +As the copilot, you can execute MCP tools directly on behalf of the user using the \`executeMcpTool\` builtin. This allows you to use MCP tools without creating an agent. + +### When to Execute MCP Tools Directly +- User asks you to perform a task that an MCP tool can handle (web search, file operations, API calls, etc.) +- User wants immediate results from an MCP tool without setting up an agent +- You need to test or demonstrate an MCP tool's functionality +- You're helping the user accomplish a one-time task + +### Workflow for Executing MCP Tools +1. **Discover available servers**: Use \`listMcpServers\` to see what MCP servers are configured +2. **List tools from a server**: Use \`listMcpTools\` with the server name to see available tools and their schemas +3. **CAREFULLY EXAMINE THE SCHEMA**: Look at the \`inputSchema\` to understand exactly what parameters are required +4. **Execute the tool**: Use \`executeMcpTool\` with the server name, tool name, and required arguments (matching the schema exactly) +5. **Return results**: Present the results to the user in a helpful format + +### CRITICAL: Schema Matching + +**ALWAYS** examine the \`inputSchema\` from \`listMcpTools\` before calling \`executeMcpTool\`. + +The schema tells you: +- What parameters are required (check the \`"required"\` array) +- What type each parameter should be (string, number, boolean, object, array) +- Parameter descriptions and examples + +**Example schema from listMcpTools:** +\`\`\`json +{ + "name": "COMPOSIO_SEARCH_WEB", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query" + }, + "limit": { + "type": "number", + "description": "Number of results" + } + }, + "required": ["query"] + } +} +\`\`\` + +**Correct executeMcpTool call:** +\`\`\`json +{ + "serverName": "composio", + "toolName": "COMPOSIO_SEARCH_WEB", + "arguments": { + "query": "elon musk latest news" + } +} +\`\`\` + +**WRONG - Missing arguments:** +\`\`\`json +{ + "serverName": "composio", + "toolName": "COMPOSIO_SEARCH_WEB" +} +\`\`\` + +**WRONG - Wrong parameter name:** +\`\`\`json +{ + "serverName": "composio", + "toolName": "COMPOSIO_SEARCH_WEB", + "arguments": { + "search": "elon musk" // Wrong! Should be "query" + } +} +\`\`\` + +### Example: Using Firecrawl to Search the Web + +**Step 1: List servers** +\`\`\`json +// Call: listMcpServers +// Response: { "servers": [{"name": "firecrawl", "type": "stdio", ...}] } +\`\`\` + +**Step 2: List tools** +\`\`\`json +// Call: listMcpTools with serverName: "firecrawl" +// Response: { "tools": [{"name": "firecrawl_search", "description": "Search the web", "inputSchema": {...}}] } +\`\`\` + +**Step 3: Execute the tool** +\`\`\`json +{ + "serverName": "firecrawl", + "toolName": "firecrawl_search", + "arguments": { + "query": "latest AI news", + "limit": 5 + } +} +\`\`\` + +### Example: Using Filesystem Tool + +**Execute a filesystem read operation:** +\`\`\`json +{ + "serverName": "filesystem", + "toolName": "read_file", + "arguments": { + "path": "/path/to/file.txt" + } +} +\`\`\` + +### Tips for Executing MCP Tools +- Always check the \`inputSchema\` from \`listMcpTools\` to know what arguments are required +- Match argument types exactly (string, number, boolean, object, array) +- Provide helpful context to the user about what the tool is doing +- Handle errors gracefully and suggest alternatives if a tool fails +- For complex tasks, consider creating an agent instead of one-off tool calls + +### Discovery Pattern (Recommended) + +When a user asks for something that might be accomplished with an MCP tool: + +1. **Identify the need**: "You want to search the web? Let me check what MCP tools are available..." +2. **List servers**: Call \`listMcpServers\` +3. **Check for relevant tools**: If you find a relevant server (e.g., "firecrawl" for web search), call \`listMcpTools\` +4. **Execute the tool**: Once you find the right tool and understand its schema, call \`executeMcpTool\` +5. **Present results**: Format and explain the results to the user + +### Common MCP Servers and Their Tools + +Based on typical configurations, you might find: +- **firecrawl**: Web scraping, search, crawling (\`firecrawl_search\`, \`firecrawl_scrape\`, \`firecrawl_crawl\`) +- **filesystem**: File operations (\`read_file\`, \`write_file\`, \`list_directory\`) +- **github**: GitHub operations (\`create_issue\`, \`create_pr\`, \`search_repositories\`) +- **fetch**: HTTP requests (\`fetch\`, \`post\`) +- **time**: Time/date operations (\`get_current_time\`, \`convert_timezone\`) + +Always use \`listMcpServers\` and \`listMcpTools\` to discover what's actually available rather than assuming. ## Adding MCP Tools to Agents diff --git a/apps/cli/src/application/lib/builtin-tools.ts b/apps/cli/src/application/lib/builtin-tools.ts index 80158d77..e0c02f48 100644 --- a/apps/cli/src/application/lib/builtin-tools.ts +++ b/apps/cli/src/application/lib/builtin-tools.ts @@ -539,6 +539,122 @@ export const BuiltinTools: z.infer = { }, }, + executeMcpTool: { + description: 'Execute a specific tool from an MCP server. Use this to run MCP tools on behalf of the user. IMPORTANT: Always use listMcpTools first to get the tool\'s inputSchema, then match the required parameters exactly in the arguments field.', + inputSchema: z.object({ + serverName: z.string().describe('Name of the MCP server that provides the tool'), + toolName: z.string().describe('Name of the tool to execute'), + arguments: z.record(z.string(), z.any()).optional().describe('Arguments to pass to the tool (as key-value pairs matching the tool\'s input schema). MUST include all required parameters from the tool\'s inputSchema.'), + }), + execute: async ({ serverName, toolName, arguments: args = {} }: { serverName: string, toolName: string, arguments?: Record }) => { + let transport: any; + let client: any; + + try { + const configPath = path.join(BASE_DIR, 'config', 'mcp.json'); + const content = await fs.readFile(configPath, 'utf-8'); + const config = JSON.parse(content); + + const mcpConfig = config.mcpServers[serverName]; + if (!mcpConfig) { + return { + success: false, + message: `MCP server '${serverName}' not found in configuration. Use listMcpServers to see available servers.`, + }; + } + + // Create transport based on config type + if ('command' in mcpConfig) { + transport = new StdioClientTransport({ + command: mcpConfig.command, + args: mcpConfig.args || [], + env: mcpConfig.env || {}, + }); + } else { + try { + transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url)); + } catch { + transport = new SSEClientTransport(new URL(mcpConfig.url)); + } + } + + // Create and connect client + client = new Client({ + name: 'rowboat-copilot', + version: '1.0.0', + }); + + await client.connect(transport); + + // Get tool list to validate the tool exists and check schema + const toolsList = await client.listTools(); + const toolDef = toolsList.tools.find((t: any) => t.name === toolName); + + if (!toolDef) { + await client.close(); + transport.close(); + return { + success: false, + message: `Tool '${toolName}' not found in server '${serverName}'. Use listMcpTools to see available tools.`, + availableTools: toolsList.tools.map((t: any) => t.name), + }; + } + + // Validate required parameters + const inputSchema = toolDef.inputSchema; + if (inputSchema && inputSchema.required && Array.isArray(inputSchema.required)) { + const missingParams = inputSchema.required.filter((param: string) => !(param in args)); + if (missingParams.length > 0) { + await client.close(); + transport.close(); + return { + success: false, + message: `Missing required parameters: ${missingParams.join(', ')}`, + requiredParameters: inputSchema.required, + providedArguments: Object.keys(args), + toolSchema: inputSchema, + hint: `Use listMcpTools to see the full schema for '${toolName}' and ensure all required parameters are included in the arguments field.`, + }; + } + } + + // Call the tool + const result = await client.callTool({ + name: toolName, + arguments: args, + }); + + // Close connection + await client.close(); + transport.close(); + + return { + success: true, + serverName, + toolName, + result: result.content, + message: `Successfully executed tool '${toolName}' from server '${serverName}'`, + }; + } catch (error) { + // Ensure cleanup + try { + if (client) await client.close(); + if (transport) transport.close(); + } catch (cleanupError) { + // Ignore cleanup errors + } + + return { + success: false, + message: `Failed to execute MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`, + serverName, + toolName, + hint: 'Use listMcpTools to verify the tool exists and check its schema. Ensure all required parameters are provided in the arguments field.', + }; + } + }, + }, + executeCommand: { description: 'Execute a shell command and return the output. Use this to run bash/shell commands.', inputSchema: z.object({ From 6d21d79fbdb23c35eb90338db0d41173b02a0e34 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Tue, 2 Dec 2025 10:54:23 +0530 Subject: [PATCH 21/21] Enhance CopilotInstructions to include general capabilities for user assistance --- apps/cli/src/application/assistant/instructions.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/application/assistant/instructions.ts b/apps/cli/src/application/assistant/instructions.ts index a479025e..7d8aad14 100644 --- a/apps/cli/src/application/assistant/instructions.ts +++ b/apps/cli/src/application/assistant/instructions.ts @@ -1,7 +1,11 @@ import { skillCatalog } from "./skills/index.js"; import { WorkDir as BASE_DIR } from "../config/config.js"; -export const CopilotInstructions = `You are an intelligent workflow assistant helping users manage their workflows in ${BASE_DIR} +export const CopilotInstructions = `You are an intelligent workflow assistant helping users manage their workflows in ${BASE_DIR}. You can also help the user with general tasks. + +## General Capabilities + +In addition to Rowboat-specific workflow management, you can help users with general tasks like answering questions, explaining concepts, brainstorming ideas, solving problems, writing and debugging code, analyzing information, and providing explanations on a wide range of topics. Be conversational, helpful, and engaging. For tasks requiring external capabilities (web search, APIs, etc.), use MCP tools as described below. Use the catalog below to decide which skills to load for each user request. Before acting: - Call the \`loadSkill\` tool with the skill's name or path so you can read its guidance string.