diff --git a/.changeset/connectors-aggregate-instructions.md b/.changeset/connectors-aggregate-instructions.md new file mode 100644 index 0000000..770e12c --- /dev/null +++ b/.changeset/connectors-aggregate-instructions.md @@ -0,0 +1,7 @@ +--- +'@gemstack/connectors': patch +--- + +Aggregate each connector's `instructions` into the mounted server's metadata. + +`mountConnectors` previously read only the server-level `instructions` option and ignored `Connector.instructions`, so per-connector instructions set by `defineConnector` were silently dropped. They are now composed into the server instructions: the server-level text first, then each connector's text under a heading named after the connector. diff --git a/packages/connectors/src/index.test.ts b/packages/connectors/src/index.test.ts index c9132c0..1f2bad1 100644 --- a/packages/connectors/src/index.test.ts +++ b/packages/connectors/src/index.test.ts @@ -116,3 +116,34 @@ test('mountConnectors rejects cross-connector name collisions without namespacin // 'a' and 'b' both expose 'x'; with namespace: 'none' the names collide. assert.throws(() => mountConnectors([a, b], { namespace: 'none' })) }) + +function connectorWith(id: string, name: string, instructions: string) { + return defineConnector({ + id, + name, + instructions, + tools: [{ name: 'ping', schema: z.object({}), handle: () => 'pong' }], + }) +} + +test('mountConnectors aggregates each connector\'s instructions under its name', () => { + const Server = mountConnectors([ + connectorWith('gh', 'GitHub', 'Act on issues and PRs.'), + connectorWith('gd', 'Drive', 'Browse and share files.'), + ]) + const { instructions } = new Server().metadata() + assert.equal(instructions, '## GitHub\nAct on issues and PRs.\n\n## Drive\nBrowse and share files.') +}) + +test('mountConnectors puts server-level instructions before per-connector ones', () => { + const Server = mountConnectors([connectorWith('gh', 'GitHub', 'Act on issues and PRs.')], { + instructions: 'You have external connectors.', + }) + const { instructions } = new Server().metadata() + assert.equal(instructions, 'You have external connectors.\n\n## GitHub\nAct on issues and PRs.') +}) + +test('mountConnectors omits instructions when no connector or server sets any', () => { + const Server = mountConnectors([notesConnector()]) + assert.equal(new Server().metadata().instructions, undefined) +}) diff --git a/packages/connectors/src/mountConnectors.ts b/packages/connectors/src/mountConnectors.ts index 10680b1..1be97ab 100644 --- a/packages/connectors/src/mountConnectors.ts +++ b/packages/connectors/src/mountConnectors.ts @@ -69,7 +69,7 @@ export function mountConnectors(connectors: Connector[], options: MountOptions = const name = options.name ?? 'connectors' const version = options.version ?? '1.0.0' - const instructions = options.instructions + const instructions = composeInstructions(connectors, options.instructions) return class ConnectorsServer extends McpServer { protected override tools = toolClasses @@ -79,6 +79,22 @@ export function mountConnectors(connectors: Connector[], options: MountOptions = } } +/** + * Combine the server-level instructions with each connector's own + * {@link Connector.instructions} into the single string advertised to the agent. + * Server-level text comes first; each connector's text follows under a heading + * named after the connector, so the agent knows which tools it applies to. + */ +function composeInstructions(connectors: Connector[], serverLevel?: string): string | undefined { + const parts: string[] = [] + if (serverLevel != null && serverLevel.trim() !== '') parts.push(serverLevel.trim()) + for (const connector of connectors) { + const text = connector.instructions + if (text != null && text.trim() !== '') parts.push(`## ${connector.name}\n${text.trim()}`) + } + return parts.length > 0 ? parts.join('\n\n') : undefined +} + function makeToolClass( connector: Connector, tool: ConnectorTool,