-
Notifications
You must be signed in to change notification settings - Fork 1
feat: initialize StackOne TypeScript agent with configs and examples #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Introduces a new stackone-typescript-agent example app that uses the Vercel AI SDK + Anthropic to dynamically discover StackOne MCP tools and run an interactive CLI agent.
Changes:
- Added a new TypeScript app with scripts, TS config, and environment variable example.
- Implemented an interactive CLI agent that discovers linked accounts, lists MCP tools per account, and enables direct tool invocation via
generateText. - Added dynamic system prompt and example queries based on connected providers.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| apps/stackone-typescript-agent/src/agent.ts | Implements MCP account/tool discovery, tool execution wiring, prompt building, and CLI loop. |
| apps/stackone-typescript-agent/package.json | Adds dependencies and scripts for running/typechecking the agent. |
| apps/stackone-typescript-agent/tsconfig.json | TypeScript compiler configuration for the new app. |
| apps/stackone-typescript-agent/.env.example | Documents required environment variables for running the agent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function createVercelTool(mcpTool: McpTool): CoreTool { | ||
| const toolName = mcpTool.name; | ||
|
|
||
| return { | ||
| description: mcpTool.description || `Call the ${toolName} tool`, | ||
| parameters: z.record(z.unknown()).describe("Arguments for the tool"), | ||
| execute: async (args: Record<string, unknown>) => { |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
McpTool includes inputSchema, but createVercelTool currently uses a generic z.record(z.unknown()) and ignores the provided schema. This loses parameter shape information and removes any validation/guidance for the model. Consider translating inputSchema into a Zod schema when available (and falling back to a permissive schema only when it’s missing).
| toolToAccount.set(mcpTool.name, account.id); | ||
| tools[mcpTool.name] = createVercelTool(mcpTool); |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tools[mcpTool.name] and toolToAccount.set(mcpTool.name, …) will silently overwrite when multiple linked accounts expose the same MCP tool name (e.g., two Gmail accounts). That can route tool calls to the wrong account. Consider namespacing tool names (e.g., include account id/provider in the exposed tool name) or storing multiple accounts per tool and requiring disambiguation.
| toolToAccount.set(mcpTool.name, account.id); | |
| tools[mcpTool.name] = createVercelTool(mcpTool); | |
| // Ensure tool names are unique across accounts to avoid silent overwrites. | |
| const baseName = mcpTool.name; | |
| let uniqueToolName = baseName; | |
| if (tools[uniqueToolName]) { | |
| // First try namespacing by provider. | |
| uniqueToolName = `${account.provider}_${baseName}`; | |
| } | |
| if (tools[uniqueToolName]) { | |
| // If still colliding (e.g., multiple accounts with same provider), | |
| // append a numeric suffix until we find an available name. | |
| let counter = 2; | |
| let candidate = `${uniqueToolName}_${counter}`; | |
| while (tools[candidate]) { | |
| counter += 1; | |
| candidate = `${uniqueToolName}_${counter}`; | |
| } | |
| uniqueToolName = candidate; | |
| } | |
| toolToAccount.set(uniqueToolName, account.id); | |
| tools[uniqueToolName] = createVercelTool(mcpTool); |
| tools[mcpTool.name] = createVercelTool(mcpTool); | ||
| } | ||
|
|
||
| providerCounts[account.provider] = mcpTools.length; |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
providerCounts[account.provider] = mcpTools.length overwrites counts when there are multiple active accounts for the same provider; the summary and system prompt will be inaccurate. Accumulate counts per provider (e.g., providerCounts[provider] = (providerCounts[provider] ?? 0) + mcpTools.length).
| providerCounts[account.provider] = mcpTools.length; | |
| providerCounts[account.provider] = | |
| (providerCounts[account.provider] ?? 0) + mcpTools.length; |
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 issues found across 4 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="apps/stackone-typescript-agent/src/agent.ts">
<violation number="1" location="apps/stackone-typescript-agent/src/agent.ts:105">
P2: Using `||` here discards valid falsy tool results (0/""/false) and returns the wrapper object instead. Use nullish coalescing to preserve legitimate results.</violation>
<violation number="2" location="apps/stackone-typescript-agent/src/agent.ts:174">
P2: Tools from multiple linked accounts can overwrite each other because they’re keyed only by tool name. For users with multiple accounts on the same provider, the last account silently wins and tool calls may be routed to the wrong account.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name=".gitignore">
<violation number="1" location=".gitignore:40">
P2: Avoid ignoring lock files globally. Lockfiles should be committed so installs are reproducible across machines and CI.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| headers: { | ||
| Authorization: `Basic ${Buffer.from(STACKONE_API_KEY + ":").toString("base64")}`, | ||
| "x-account-id": accountId, | ||
| "Content-Type": "application/json", | ||
| Accept: "application/json, text/event-stream", | ||
| }, | ||
| body: JSON.stringify({ | ||
| jsonrpc: "2.0", | ||
| id: `req-${method}-${Date.now()}`, |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The request advertises Accept: application/json, text/event-stream but the response is always parsed via response.json(). If the server chooses text/event-stream (SSE), this will fail at runtime. Either request only JSON here or add handling for SSE responses.
| @@ -0,0 +1,22 @@ | |||
| { | |||
| "name": "stackone-typescript-agent", | |||
| "version": "1.0.0", | |||
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This app's package.json is missing "private": true. Other example apps in this repo (e.g. apps/oauth-redirect-proxy/package.json) mark themselves private to prevent accidental publication to npm.
| "version": "1.0.0", | |
| "version": "1.0.0", | |
| "private": true, |
| "scripts": { | ||
| "agent": "tsx src/agent.ts", | ||
| "typecheck": "tsc --noEmit" | ||
| }, |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This app defines a typecheck script, but the repo's root uses turbo run check-types (see turbo.json + root package.json). As-is, this app won't participate in the standard type-check task. Rename/add a check-types script (and optionally keep typecheck as an alias) to integrate with the monorepo workflows.
| // For mixed enums, use union of literals | ||
| const literals = schema.enum.map((v) => z.literal(v as string | number | boolean)); | ||
| const zodUnion = z.union(literals as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]); |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For non-string enums, this builds a z.union(...) from schema.enum. If the enum has only 1 value (e.g., [1] or [null]), z.union will throw because it requires at least 2 options. Handle single-value enums by returning z.literal(value) (and include null as a supported literal type).
| // For mixed enums, use union of literals | |
| const literals = schema.enum.map((v) => z.literal(v as string | number | boolean)); | |
| const zodUnion = z.union(literals as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]); | |
| // For mixed enums, use literals; handle single-value enums without z.union | |
| const literals = schema.enum.map((v) => | |
| z.literal(v as string | number | boolean | null), | |
| ); | |
| if (literals.length === 1) { | |
| const singleLiteral = literals[0]; | |
| return schema.description | |
| ? singleLiteral.describe(schema.description) | |
| : singleLiteral; | |
| } | |
| const zodUnion = z.union( | |
| literals as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]], | |
| ); |
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
…age-lock.json for stackone-typescript-agent
StuBehan
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, some of the co-pilot comments might be actionable but not gonna block for them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 6 changed files in this pull request and generated 6 comments.
Files not reviewed (1)
- apps/stackone-typescript-agent/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const accountId = toolToAccount.get(toolName); | ||
| if (!accountId) { |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callTool() looks up toolToAccount by the tool name it receives. However tools are registered under a namespaced key (provider:accountId:...) while createVercelTool() uses the underlying mcpTool.name. This will cause Unknown tool at runtime. Fix by keying lookups on the registered tool key and keeping a separate mapping for the MCP tool name.
| const accountId = toolToAccount.get(toolName); | |
| if (!accountId) { | |
| // First, try direct lookup (in case the map is keyed by the raw tool name) | |
| let accountId = toolToAccount.get(toolName); | |
| // If not found, attempt to resolve by matching the suffix of a namespaced key | |
| if (!accountId) { | |
| for (const [registeredKey, mappedAccountId] of toolToAccount.entries()) { | |
| const registeredName = registeredKey.split(":").pop(); | |
| if (registeredName === toolName) { | |
| accountId = mappedAccountId; | |
| break; | |
| } | |
| } | |
| } | |
| if (!accountId) { |
| const result = (await mcpRequest( | ||
| "tools/call", | ||
| { name: toolName, arguments: args }, | ||
| accountId |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In tools/call, you send { name: toolName }. If toolName is the namespaced key, MCP will likely not recognize it; if it’s the raw MCP name, it won’t match your toolToAccount keys. Store { accountId, mcpName } per registered tool key and use mcpName in the MCP request.
| */ | ||
| function createVercelTool(mcpTool: McpTool): CoreTool { | ||
| const toolName = mcpTool.name; |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createVercelTool() binds toolName to mcpTool.name, but the tool is actually exposed to the model under a different key (provider:accountId:...). Pass the exposed tool key into createVercelTool (for logging/description) so what the model calls matches what you display.
| */ | |
| function createVercelTool(mcpTool: McpTool): CoreTool { | |
| const toolName = mcpTool.name; | |
| * | |
| * @param mcpTool - The underlying MCP tool definition. | |
| * @param exposedToolKey - The key under which this tool is exposed to the model | |
| * (e.g. "provider:accountId:toolName"). Used for logging | |
| * and descriptions so they match what the model calls. | |
| */ | |
| function createVercelTool(mcpTool: McpTool, exposedToolKey?: string): CoreTool { | |
| const toolName = exposedToolKey ?? mcpTool.name; |
| Tool names follow the pattern: provider_action (e.g., gmail_list_messages, drive_list_files). | ||
| Use tool names and descriptions to understand their capabilities. | ||
| You have conversation history - you can reference previous results. |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The system prompt says tool names follow provider_action (e.g. gmail_list_messages), but the actual registered tool keys are provider:accountId:mcpTool.name. This mismatch can cause the model to call non-existent tools. Update the prompt to describe the real naming scheme (and include an example that matches).
| // Add response to history | ||
| conversationHistory.push({ role: "assistant", content: text }); | ||
|
|
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conversation history only persists user/assistant text (conversationHistory.push({ role: "assistant", content: text })) and does not store tool calls/results, but the system prompt claims prior results are available. Persist tool outputs (or a trimmed summary) into conversationHistory after each run so follow-up questions can reference them reliably.
Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 6 changed files in this pull request and generated 3 comments.
Files not reviewed (1)
- apps/stackone-typescript-agent/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Authorization: `Basic ${Buffer.from(STACKONE_API_KEY + ":").toString("base64")}`, | ||
| "x-account-id": accountId, | ||
| "Content-Type": "application/json", | ||
| Accept: "application/json, text/event-stream", |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The request advertises Accept: application/json, text/event-stream but the response is always parsed via response.json(). If the MCP endpoint responds with text/event-stream (SSE), this will throw. Either remove text/event-stream from Accept or branch on the Content-Type and implement SSE parsing.
| Accept: "application/json, text/event-stream", | |
| Accept: "application/json", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
text/event-stream is required when fetching from https://api.stackone.com/mcp
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
|
Refactored the code and reviewed via Copilot a couple of times; it's definitely cleaner now. Thanks for pointing it out @StuBehan, didn't really look at the comments well. It seems the merge is still blocked, though. Needs a "reviewer with write access" |
This pull request introduces a new TypeScript-based StackOne AI Agent example application that uses the Vercel AI SDK and supports dynamic tool creation from StackOne's MCP endpoint. The agent allows direct tool invocation (without a meta-tool) and features an interactive CLI for user interaction. The implementation includes configuration, dependencies, agent logic, and TypeScript project setup.
The most important changes are:
New Application: StackOne TypeScript Agent
stackone-typescript-agentapp, including configuration files (.env.example,package.json,tsconfig.json) and a complete agent implementation insrc/agent.ts. [1] [2] [3] [4]Agent Implementation and Features (
src/agent.ts):Project Configuration and Dependencies:
.env.examplefor required environment variables, including API keys for StackOne and Anthropic.package.jsonwith necessary dependencies (@ai-sdk/anthropic,ai,dotenv,zod, etc.) and scripts for running and type-checking the agent.tsconfig.jsonfor strict typing, ES module support, and output settings.Summary by cubic
Adds a TypeScript StackOne agent example that discovers tools from MCP for each linked account and calls them directly via the Vercel SDK. Includes an interactive CLI with history and a provider-aware system prompt.
New Features
Dependencies
Written for commit 1f9f779. Summary will update on new commits.