From dc9f6e5092380e699de71f5f005ae33b7d562559 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:30:51 +0000 Subject: [PATCH 1/8] feat(toolsets): add accountIds option to StackOneToolSetConfig Allow passing multiple account IDs when initialising StackOneToolSet via the constructor, eliminating the need to call setAccounts() separately after instantiation. - Add accountIds property to StackOneToolSetConfig interface with JSDoc documentation - Initialise accountIds from config in constructor - Update constructor JSDoc to reflect support for multiple account IDs This change enables a more ergonomic API for multi-account setups: ```typescript const toolset = new StackOneToolSet({ apiKey: 'key', accountIds: ['acc1', 'acc2', 'acc3'], }); ``` Closes #251 --- src/toolsets.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/toolsets.ts b/src/toolsets.ts index 483dbce..51ba469 100644 --- a/src/toolsets.ts +++ b/src/toolsets.ts @@ -93,6 +93,12 @@ export interface BaseToolSetConfig { export interface StackOneToolSetConfig extends BaseToolSetConfig { apiKey?: string; accountId?: string; + /** + * Array of account IDs for filtering tools across multiple accounts + * When provided, tools will be fetched for all specified accounts + * @example ['account-1', 'account-2'] + */ + accountIds?: string[]; strict?: boolean; } @@ -137,8 +143,8 @@ export class StackOneToolSet { private accountIds: string[] = []; /** - * Initialise StackOne toolset with API key and optional account ID - * @param config Configuration object containing API key and optional account ID + * Initialise StackOne toolset with API key and optional account ID(s) + * @param config Configuration object containing API key and optional account ID(s) */ constructor(config?: StackOneToolSetConfig) { const apiKey = config?.apiKey || process.env.STACKONE_API_KEY; @@ -176,6 +182,7 @@ export class StackOneToolSet { this.headers = configHeaders; this.rpcClient = config?.rpcClient; this.accountId = accountId; + this.accountIds = config?.accountIds ?? []; // Set Authentication headers if provided if (this.authentication) { From 70191def5eb403ab11a116f144522a8de58f0fed Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:31:03 +0000 Subject: [PATCH 2/8] test(toolsets): add tests for accountIds constructor option Add comprehensive tests verifying the new accountIds configuration: - Test initialising with multiple account IDs from constructor - Test default empty array when accountIds not provided - Test coexistence of accountId and accountIds in constructor - Test fetchTools uses constructor accountIds when present - Test setAccounts() overrides constructor accountIds These tests ensure accountIds from the constructor integrates correctly with existing setAccounts() and fetchTools() behaviours. --- src/toolsets.test.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/toolsets.test.ts b/src/toolsets.test.ts index f905407..45a111e 100644 --- a/src/toolsets.test.ts +++ b/src/toolsets.test.ts @@ -83,6 +83,38 @@ describe('StackOneToolSet', () => { expect(toolset.accountIds).toEqual(['account-1', 'account-2']); }); + it('should initialise with multiple account IDs from constructor', () => { + const toolset = new StackOneToolSet({ + apiKey: 'custom_key', + accountIds: ['account-1', 'account-2', 'account-3'], + }); + + // @ts-expect-error - Accessing private property for testing + expect(toolset.accountIds).toEqual(['account-1', 'account-2', 'account-3']); + }); + + it('should initialise with empty accountIds array when not provided', () => { + const toolset = new StackOneToolSet({ apiKey: 'custom_key' }); + + // @ts-expect-error - Accessing private property for testing + expect(toolset.accountIds).toEqual([]); + }); + + it('should allow both accountId and accountIds in constructor', () => { + const toolset = new StackOneToolSet({ + apiKey: 'custom_key', + accountId: 'primary-account', + accountIds: ['account-1', 'account-2'], + }); + + // Single accountId should be set in headers + // @ts-expect-error - Accessing private property for testing + expect(toolset.headers['x-account-id']).toBe('primary-account'); + // Multiple accountIds should be stored separately + // @ts-expect-error - Accessing private property for testing + expect(toolset.accountIds).toEqual(['account-1', 'account-2']); + }); + it('should set baseUrl from config', () => { const toolset = new StackOneToolSet({ apiKey: 'custom_key', @@ -283,6 +315,51 @@ describe('StackOneToolSet', () => { expect(toolNames).toContain('meta_collect_tool_feedback'); }); + it('uses accountIds from constructor when no accountIds provided in fetchTools', async () => { + const toolset = new StackOneToolSet({ + baseUrl: 'https://api.stackone-dev.com', + apiKey: 'test-key', + accountIds: ['acc1', 'acc2'], + }); + + // Fetch without accountIds - should use constructor accountIds + const tools = await toolset.fetchTools(); + + // Should fetch tools for 2 accounts from constructor + // acc1 has 2 tools, acc2 has 2 tools, + 1 feedback tool = 5 + expect(tools.length).toBe(5); + const toolNames = tools.toArray().map((t) => t.name); + expect(toolNames).toContain('acc1_tool_1'); + expect(toolNames).toContain('acc1_tool_2'); + expect(toolNames).toContain('acc2_tool_1'); + expect(toolNames).toContain('acc2_tool_2'); + expect(toolNames).toContain('meta_collect_tool_feedback'); + }); + + it('setAccounts overrides constructor accountIds', async () => { + const toolset = new StackOneToolSet({ + baseUrl: 'https://api.stackone-dev.com', + apiKey: 'test-key', + accountIds: ['acc1'], + }); + + // Override with setAccounts + toolset.setAccounts(['acc2', 'acc3']); + + // Fetch without accountIds - should use setAccounts, not constructor + const tools = await toolset.fetchTools(); + + // Should fetch tools for acc2 and acc3 (not acc1) + // acc2 has 2 tools, acc3 has 1 tool, + 1 feedback tool = 4 + expect(tools.length).toBe(4); + const toolNames = tools.toArray().map((t) => t.name); + expect(toolNames).not.toContain('acc1_tool_1'); + expect(toolNames).toContain('acc2_tool_1'); + expect(toolNames).toContain('acc2_tool_2'); + expect(toolNames).toContain('acc3_tool_1'); + expect(toolNames).toContain('meta_collect_tool_feedback'); + }); + it('overrides setAccounts when accountIds provided in fetchTools', async () => { const toolset = new StackOneToolSet({ baseUrl: 'https://api.stackone-dev.com', From e15da2937cb0ce9f1fb5e7a9c5e9c97bcd18e0a6 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:39:25 +0000 Subject: [PATCH 3/8] refactor(toolsets): use MergeExclusive for mutually exclusive account options Use type-fest's MergeExclusive to enforce that either accountId (single) or accountIds (multiple) can be provided, but not both simultaneously. This improves type safety by catching invalid configurations at compile time rather than runtime. The API now clearly communicates the intended usage pattern through the type system. Before (both allowed - confusing behaviour): ```typescript new StackOneToolSet({ accountId: 'acc1', accountIds: ['acc2', 'acc3'], // Which takes precedence? }); ``` After (type error - clear contract): ```typescript // Valid: single account new StackOneToolSet({ accountId: 'acc1' }); // Valid: multiple accounts new StackOneToolSet({ accountIds: ['acc1', 'acc2'] }); // Type error: cannot use both new StackOneToolSet({ accountId: 'acc1', accountIds: ['acc2'] }); ``` --- src/toolsets.ts | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/toolsets.ts b/src/toolsets.ts index 51ba469..bf22628 100644 --- a/src/toolsets.ts +++ b/src/toolsets.ts @@ -1,4 +1,5 @@ import { defu } from 'defu'; +import type { MergeExclusive } from 'type-fest'; import { DEFAULT_BASE_URL, UNIFIED_API_PREFIX } from './consts'; import { createFeedbackTool } from './feedback'; import { type StackOneHeaders, normaliseHeaders, stackOneHeadersSchema } from './headers'; @@ -88,20 +89,47 @@ export interface BaseToolSetConfig { } /** - * Configuration for StackOne toolset + * Configuration with a single account ID */ -export interface StackOneToolSetConfig extends BaseToolSetConfig { - apiKey?: string; - accountId?: string; +interface SingleAccountConfig { + /** + * Single account ID for StackOne API operations + * Use this when working with a single account + */ + accountId: string; +} + +/** + * Configuration with multiple account IDs + */ +interface MultipleAccountsConfig { /** * Array of account IDs for filtering tools across multiple accounts * When provided, tools will be fetched for all specified accounts * @example ['account-1', 'account-2'] */ - accountIds?: string[]; + accountIds: string[]; +} + +/** + * Account configuration options - either single accountId or multiple accountIds, but not both + */ +type AccountConfig = MergeExclusive; + +/** + * Base configuration for StackOne toolset (without account options) + */ +interface StackOneToolSetBaseConfig extends BaseToolSetConfig { + apiKey?: string; strict?: boolean; } +/** + * Configuration for StackOne toolset + * Accepts either accountId (single) or accountIds (multiple), but not both + */ +export type StackOneToolSetConfig = StackOneToolSetBaseConfig & Partial; + /** * Options for filtering tools when fetching from MCP */ From 2ab55cd648b69b679d13c3d4edfe5e8278eab6f8 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:39:32 +0000 Subject: [PATCH 4/8] test(toolsets): update tests for MergeExclusive account config Update test to verify that the type system enforces mutual exclusivity between accountId and accountIds. Replace the test that allowed both with a new test demonstrating the correct API usage patterns. - Test single accountId configuration works correctly - Test multiple accountIds configuration works correctly - Document that combining both is a type error (prevented at compile time) --- src/toolsets.test.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/toolsets.test.ts b/src/toolsets.test.ts index 45a111e..a9444c8 100644 --- a/src/toolsets.test.ts +++ b/src/toolsets.test.ts @@ -100,19 +100,34 @@ describe('StackOneToolSet', () => { expect(toolset.accountIds).toEqual([]); }); - it('should allow both accountId and accountIds in constructor', () => { - const toolset = new StackOneToolSet({ + it('should not allow both accountId and accountIds in constructor (type check)', () => { + // This test verifies the type system prevents using both accountId and accountIds + // The following would be a type error: + // new StackOneToolSet({ + // apiKey: 'custom_key', + // accountId: 'primary-account', + // accountIds: ['account-1', 'account-2'], + // }); + + // Valid: only accountId + const toolsetSingle = new StackOneToolSet({ apiKey: 'custom_key', accountId: 'primary-account', - accountIds: ['account-1', 'account-2'], }); + // @ts-expect-error - Accessing private property for testing + expect(toolsetSingle.headers['x-account-id']).toBe('primary-account'); + // @ts-expect-error - Accessing private property for testing + expect(toolsetSingle.accountIds).toEqual([]); - // Single accountId should be set in headers + // Valid: only accountIds + const toolsetMultiple = new StackOneToolSet({ + apiKey: 'custom_key', + accountIds: ['account-1', 'account-2'], + }); // @ts-expect-error - Accessing private property for testing - expect(toolset.headers['x-account-id']).toBe('primary-account'); - // Multiple accountIds should be stored separately + expect(toolsetMultiple.headers['x-account-id']).toBeUndefined(); // @ts-expect-error - Accessing private property for testing - expect(toolset.accountIds).toEqual(['account-1', 'account-2']); + expect(toolsetMultiple.accountIds).toEqual(['account-1', 'account-2']); }); it('should set baseUrl from config', () => { From cac507c097f57bdd063c783942072281b6a48cb6 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:41:08 +0000 Subject: [PATCH 5/8] test(toolsets): add type-level tests for StackOneToolSetConfig Add comprehensive type tests using vitest's expectTypeOf to verify the MergeExclusive behaviour of account configuration options: - Test that accountId alone is valid - Test that accountIds alone is valid - Test that neither is valid (optional) - Test that both together is rejected by the type system - Verify accountId is typed as string | undefined - Verify accountIds is typed as string[] | undefined --- src/toolsets.test-d.ts | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/toolsets.test-d.ts diff --git a/src/toolsets.test-d.ts b/src/toolsets.test-d.ts new file mode 100644 index 0000000..2a7642f --- /dev/null +++ b/src/toolsets.test-d.ts @@ -0,0 +1,44 @@ +import { expectTypeOf } from 'vitest'; +import type { StackOneToolSetConfig } from './toolsets'; + +// Valid configurations - only accountId +test('StackOneToolSetConfig accepts only accountId', () => { + expectTypeOf<{ + apiKey: string; + accountId: string; + }>().toExtend(); +}); + +// Valid configurations - only accountIds +test('StackOneToolSetConfig accepts only accountIds', () => { + expectTypeOf<{ + apiKey: string; + accountIds: string[]; + }>().toExtend(); +}); + +// Valid configurations - neither accountId nor accountIds +test('StackOneToolSetConfig accepts neither accountId nor accountIds', () => { + expectTypeOf<{ + apiKey: string; + }>().toExtend(); +}); + +// Invalid configuration - both accountId and accountIds should NOT extend +test('StackOneToolSetConfig rejects both accountId and accountIds', () => { + expectTypeOf<{ + apiKey: string; + accountId: string; + accountIds: string[]; + }>().not.toExtend(); +}); + +// Verify accountId can be string or undefined +test('accountId is typed as string | undefined', () => { + expectTypeOf().toEqualTypeOf(); +}); + +// Verify accountIds can be string[] or undefined +test('accountIds is typed as string[] | undefined', () => { + expectTypeOf().toEqualTypeOf(); +}); From c143b14cf83775c44cf7e26ded2216c51adbc814 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:46:58 +0000 Subject: [PATCH 6/8] refactor(toolset): use simplifyDeep --- src/toolsets.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/toolsets.ts b/src/toolsets.ts index bf22628..07f58d5 100644 --- a/src/toolsets.ts +++ b/src/toolsets.ts @@ -1,5 +1,5 @@ import { defu } from 'defu'; -import type { MergeExclusive } from 'type-fest'; +import type { MergeExclusive, SimplifyDeep } from 'type-fest'; import { DEFAULT_BASE_URL, UNIFIED_API_PREFIX } from './consts'; import { createFeedbackTool } from './feedback'; import { type StackOneHeaders, normaliseHeaders, stackOneHeadersSchema } from './headers'; @@ -114,7 +114,7 @@ interface MultipleAccountsConfig { /** * Account configuration options - either single accountId or multiple accountIds, but not both */ -type AccountConfig = MergeExclusive; +type AccountConfig = SimplifyDeep>; /** * Base configuration for StackOne toolset (without account options) From a5e41b7df2e148446ce89d755459b1e431532b0c Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:48:16 +0000 Subject: [PATCH 7/8] feat(toolsets): add runtime validation for mutually exclusive account options Add runtime check to throw ToolSetConfigError when both accountId and accountIds are provided simultaneously. This protects JavaScript users and scenarios where TypeScript is bypassed (e.g., using 'as any'). The validation uses != null to check for both null and undefined in a single comparison, ensuring the error is thrown regardless of how the invalid configuration is constructed. --- src/toolsets.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/toolsets.ts b/src/toolsets.ts index 07f58d5..ad31de4 100644 --- a/src/toolsets.ts +++ b/src/toolsets.ts @@ -175,6 +175,13 @@ export class StackOneToolSet { * @param config Configuration object containing API key and optional account ID(s) */ constructor(config?: StackOneToolSetConfig) { + // Validate mutually exclusive account options + if (config?.accountId != null && config?.accountIds != null) { + throw new ToolSetConfigError( + 'Cannot provide both accountId and accountIds. Use accountId for a single account or accountIds for multiple accounts.', + ); + } + const apiKey = config?.apiKey || process.env.STACKONE_API_KEY; if (!apiKey && config?.strict) { From 9b614e2845757794b3c4e48cc074a55cc4c2c61d Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:48:23 +0000 Subject: [PATCH 8/8] test(toolsets): add runtime validation test for mutually exclusive accounts Add test to verify ToolSetConfigError is thrown when both accountId and accountIds are provided at runtime. Uses 'as never' to bypass TypeScript type checking and simulate JavaScript usage scenarios. --- src/toolsets.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/toolsets.test.ts b/src/toolsets.test.ts index a9444c8..735083d 100644 --- a/src/toolsets.test.ts +++ b/src/toolsets.test.ts @@ -130,6 +130,24 @@ describe('StackOneToolSet', () => { expect(toolsetMultiple.accountIds).toEqual(['account-1', 'account-2']); }); + it('should throw error when both accountId and accountIds are provided at runtime', () => { + // Runtime validation for JavaScript users or when TypeScript is bypassed + expect(() => { + new StackOneToolSet({ + apiKey: 'custom_key', + accountId: 'primary-account', + accountIds: ['account-1', 'account-2'], + } as never); // Use 'as never' to bypass TypeScript for runtime test + }).toThrow(ToolSetConfigError); + expect(() => { + new StackOneToolSet({ + apiKey: 'custom_key', + accountId: 'primary-account', + accountIds: ['account-1', 'account-2'], + } as never); + }).toThrow(/Cannot provide both accountId and accountIds/); + }); + it('should set baseUrl from config', () => { const toolset = new StackOneToolSet({ apiKey: 'custom_key',