From 0324e0be469749cb21e7cd83038b6581eada4602 Mon Sep 17 00:00:00 2001 From: ContextVM Date: Tue, 19 May 2026 17:53:45 +0200 Subject: [PATCH] feat(transport): add category tag support to common schema announcements Add optional categories parameter to CommonToolSchemasOptions that enables including CEP-15 category tags in tools/list event announcements. Categories are normalized (trimmed whitespace) and deduplicated before being appended as 't' tags after the common-schema meta namespace tag. Includes unit and integration tests for the new functionality. --- .changeset/eighty-melons-attend.md | 11 +++ contextvm-docs | 2 +- src/transport/nostr-server-transport.test.ts | 70 +++++++++++++++++++ .../server-transport-common-schemas.test.ts | 68 ++++++++++++++++++ .../server-transport-common-schemas.ts | 29 +++++++- 5 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 .changeset/eighty-melons-attend.md diff --git a/.changeset/eighty-melons-attend.md b/.changeset/eighty-melons-attend.md new file mode 100644 index 0000000..098133d --- /dev/null +++ b/.changeset/eighty-melons-attend.md @@ -0,0 +1,11 @@ +--- +'@contextvm/sdk': patch +--- + +feat(transport): add category tag support to common schema announcements + +Add optional categories parameter to CommonToolSchemasOptions that enables +including CEP-15 category tags in tools/list event announcements. Categories +are normalized (trimmed whitespace) and deduplicated before being appended +as 't' tags after the common-schema meta namespace tag. Includes unit and +integration tests for the new functionality. \ No newline at end of file diff --git a/contextvm-docs b/contextvm-docs index 4860224..bb13dcb 160000 --- a/contextvm-docs +++ b/contextvm-docs @@ -1 +1 @@ -Subproject commit 48602243922852c7fc2ed48720b996aaa2c05b70 +Subproject commit bb13dcb304c9e087aa8a4cdddcb2a9d9d55ea27d diff --git a/src/transport/nostr-server-transport.test.ts b/src/transport/nostr-server-transport.test.ts index c890fde..e14831e 100644 --- a/src/transport/nostr-server-transport.test.ts +++ b/src/transport/nostr-server-transport.test.ts @@ -2013,4 +2013,74 @@ describe.serial('NostrServerTransport', () => { }, 15000, ); + + test.serial( + 'withCommonToolSchemas includes configured CEP-15 category tags on announced tools/list events', + async () => { + const serverPrivateKey = bytesToHex(generateSecretKey()); + const serverPublicKey = getPublicKey(hexToBytes(serverPrivateKey)); + const uniqueSuffix = Math.random().toString(36).substring(2, 8); + const commonToolName = `translate_text_${uniqueSuffix}`; + + const server = new McpServer({ + name: 'Common Schema Category Server', + version: '1.0.0', + }); + + server.registerTool( + commonToolName, + { + title: 'Translate Text', + description: 'Translate text between languages', + inputSchema: { + text: z.string(), + targetLanguage: z.string(), + }, + }, + async ({ text, targetLanguage }) => ({ + content: [ + { + type: 'text', + text: `${targetLanguage}: ${text}`, + }, + ], + }), + ); + + const transport = new NostrServerTransport({ + signer: new PrivateKeySigner(serverPrivateKey), + relayHandler: new ApplesauceRelayPool([relayUrl]), + serverInfo: { name: 'Common Schema Category Server' }, + isPublicServer: true, + encryptionMode: EncryptionMode.DISABLED, + }); + + withCommonToolSchemas(transport, { + tools: [{ name: commonToolName }], + categories: ['translation', ' translation ', '', 'language-tools'], + }); + + await server.connect(transport); + + const relayPool = new ApplesauceRelayPool([relayUrl]); + await relayPool.connect(); + + const toolsListEvent = await waitForNostrEvent({ + relayPool, + filters: [{ kinds: [TOOLS_LIST_KIND], authors: [serverPublicKey] }], + where: () => true, + }); + + const tTags = toolsListEvent.tags.filter((tag) => tag[0] === 't'); + + expect(tTags).toEqual([ + ['t', 'translation'], + ['t', 'language-tools'], + ]); + + await server.close(); + await relayPool.disconnect(); + }, + 15000, + ); }); diff --git a/src/transport/server-transport-common-schemas.test.ts b/src/transport/server-transport-common-schemas.test.ts index a1e4ab0..7c69641 100644 --- a/src/transport/server-transport-common-schemas.test.ts +++ b/src/transport/server-transport-common-schemas.test.ts @@ -215,6 +215,74 @@ describe('createCommonSchemaAnnouncementTagsProducer', () => { ]); }); + test('appends deduplicated category tags after the common-schema tags', () => { + const result: ListToolsResult = { + tools: [ + { + name: 'translate_text', + title: 'Translate Text', + inputSchema: { + type: 'object', + properties: { + text: { type: 'string' }, + }, + required: ['text'], + }, + }, + ], + }; + + const produceTags = createCommonSchemaAnnouncementTagsProducer({ + tools: [{ name: 'translate_text' }], + categories: [ + 'translation', + ' translation ', + '', + 'language-tools', + 'translation', + ], + }); + + expect(produceTags(result)).toEqual([ + [ + 'i', + computeCommonSchemaHash({ + name: 'translate_text', + inputSchema: result.tools[0]!.inputSchema, + }), + 'translate_text', + ], + ['k', COMMON_SCHEMA_META_NAMESPACE], + ['t', 'translation'], + ['t', 'language-tools'], + ]); + }); + + test('does not emit category tags when no opted-in common-schema tools are present', () => { + const result: ListToolsResult = { + tools: [ + { + name: 'bespoke_tool', + title: 'Bespoke Tool', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string' }, + }, + required: ['query'], + }, + }, + ], + }; + + const produceTags = createCommonSchemaAnnouncementTagsProducer({ + tools: [{ name: 'translate_text' }], + categories: ['translation'], + }); + + expect(produceTags(result)).toEqual([]); + }); + test('returns no tags when no common-schema tools are present', () => { const result: ListToolsResult = { tools: [ diff --git a/src/transport/server-transport-common-schemas.ts b/src/transport/server-transport-common-schemas.ts index e89892b..3b2d9e2 100644 --- a/src/transport/server-transport-common-schemas.ts +++ b/src/transport/server-transport-common-schemas.ts @@ -9,6 +9,7 @@ export interface CommonSchemaToolConfig { export interface CommonToolSchemasOptions { tools: CommonSchemaToolConfig[]; + categories?: string[]; } function isPlainObject(value: unknown): value is Record { @@ -72,6 +73,27 @@ function getCommonToolNames( return new Set(options.tools.map((tool) => tool.name)); } +function getAnnouncementCategories( + options: CommonToolSchemasOptions, +): string[] { + const categories = options.categories ?? []; + const seen = new Set(); + const normalizedCategories: string[] = []; + + categories.forEach((category) => { + const normalizedCategory = category.trim(); + + if (!normalizedCategory || seen.has(normalizedCategory)) { + return; + } + + seen.add(normalizedCategory); + normalizedCategories.push(normalizedCategory); + }); + + return normalizedCategories; +} + /** * Creates a pure transformer that enriches opted-in `tools/list` results with CEP-15 schema hashes. */ @@ -133,6 +155,7 @@ export function createCommonSchemaAnnouncementTagsProducer( options: CommonToolSchemasOptions, ): (result: ListToolsResult) => string[][] { const commonToolNames = getCommonToolNames(options); + const categories = getAnnouncementCategories(options); return (result: ListToolsResult): string[][] => { if (!commonToolNames.size) { @@ -151,7 +174,11 @@ export function createCommonSchemaAnnouncementTagsProducer( return []; } - return [...iTags, ['k', COMMON_SCHEMA_META_NAMESPACE]]; + return [ + ...iTags, + ['k', COMMON_SCHEMA_META_NAMESPACE], + ...categories.map((category) => ['t', category]), + ]; }; }