diff --git a/.changeset/oss-telemetry-client-metadata.md b/.changeset/oss-telemetry-client-metadata.md new file mode 100644 index 0000000..3fe708f --- /dev/null +++ b/.changeset/oss-telemetry-client-metadata.md @@ -0,0 +1,6 @@ +--- +"@ontos-ai/knowhere-sdk": patch +"@ontos-ai/knowhere-mcp": patch +--- + +Auto-attach official `created_by_client` / `client_version` document metadata on job creates so OSS telemetry can attribute client mix. Caller-provided metadata still wins. diff --git a/packages/mcp/src/__tests__/document-metadata.test.ts b/packages/mcp/src/__tests__/document-metadata.test.ts new file mode 100644 index 0000000..e0ff943 --- /dev/null +++ b/packages/mcp/src/__tests__/document-metadata.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { MCP_DOCUMENT_METADATA_DEFAULTS } from '../document-metadata.js'; +import { VERSION } from '../version.js'; + +describe('MCP_DOCUMENT_METADATA_DEFAULTS', () => { + it('identifies the MCP client with the package version', () => { + expect(MCP_DOCUMENT_METADATA_DEFAULTS).toEqual({ + createdByClient: 'mcp', + clientVersion: VERSION, + }); + }); +}); diff --git a/packages/mcp/src/__tests__/mcp.test.ts b/packages/mcp/src/__tests__/mcp.test.ts index 565c08d..4e64b8a 100644 --- a/packages/mcp/src/__tests__/mcp.test.ts +++ b/packages/mcp/src/__tests__/mcp.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi, type Mock } from 'vitest'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { MCP_DOCUMENT_METADATA_DEFAULTS } from '../document-metadata.js'; import { createKnowhereMcpServer } from '../index.js'; import type { Knowhere, @@ -127,6 +128,7 @@ describe('knowhere MCP wrapper', () => { namespace: undefined, localDocumentId: 'local-report', dataId: undefined, + documentMetadata: { ...MCP_DOCUMENT_METADATA_DEFAULTS }, model: 'base', ocr: true, docType: undefined, @@ -289,6 +291,7 @@ describe('knowhere MCP wrapper', () => { namespace: undefined, localDocumentId: 'local-report', dataId: undefined, + documentMetadata: { ...MCP_DOCUMENT_METADATA_DEFAULTS }, model: 'advanced', ocr: undefined, docType: undefined, diff --git a/packages/mcp/src/document-metadata.ts b/packages/mcp/src/document-metadata.ts new file mode 100644 index 0000000..d0086c8 --- /dev/null +++ b/packages/mcp/src/document-metadata.ts @@ -0,0 +1,11 @@ +import { VERSION } from './version.js'; + +/** + * Official MCP client identity for job `document_metadata`. + * Caller-provided keys win; defaults fill missing keys only when merged + * upstream by the Node SDK `jobs.create` helper. + */ +export const MCP_DOCUMENT_METADATA_DEFAULTS = { + createdByClient: 'mcp', + clientVersion: VERSION, +} as const; diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 95a543a..db604f7 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -6,7 +6,6 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { DiskParsedDocumentStorage, Knowhere, - VERSION, ValidationError, type AuthTokenProvider, type Knowledge, @@ -16,7 +15,9 @@ import { import * as z from 'zod/v4'; import type { Permission } from './auth.js'; +import { MCP_DOCUMENT_METADATA_DEFAULTS } from './document-metadata.js'; import { createKnowhereToolResult } from './tool-result-formatter.js'; +import { VERSION } from './version.js'; const parsingParamsSchema = z .object({ @@ -83,6 +84,7 @@ export async function createKnowhereMcpServer( namespace: input.namespace, localDocumentId: input.localDocumentId, dataId: input.dataId, + documentMetadata: { ...MCP_DOCUMENT_METADATA_DEFAULTS }, ...toFlatParsingParams(input.parsingParams), }), ), @@ -111,6 +113,7 @@ export async function createKnowhereMcpServer( namespace: input.namespace, localDocumentId: input.localDocumentId, dataId: input.dataId, + documentMetadata: { ...MCP_DOCUMENT_METADATA_DEFAULTS }, ...toFlatParsingParams(input.parsingParams), }), ), diff --git a/packages/mcp/src/version.ts b/packages/mcp/src/version.ts new file mode 100644 index 0000000..dc3edc9 --- /dev/null +++ b/packages/mcp/src/version.ts @@ -0,0 +1 @@ +export const VERSION = '2.1.4'; diff --git a/src/index.ts b/src/index.ts index b73a350..4ab1ef3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,12 @@ export { Knowhere, Knowhere as default } from './client.js'; // Version export { VERSION } from './version.js'; +// Document metadata helpers (official-client telemetry defaults) +export { + mergeDocumentMetadataDefaults, + NODE_SDK_DOCUMENT_METADATA_DEFAULTS, +} from './lib/document-metadata.js'; + // Types export type { // Client types diff --git a/src/lib/__tests__/document-metadata.test.ts b/src/lib/__tests__/document-metadata.test.ts new file mode 100644 index 0000000..59d433a --- /dev/null +++ b/src/lib/__tests__/document-metadata.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; + +import { VERSION } from '../../version.js'; +import { + mergeDocumentMetadataDefaults, + NODE_SDK_DOCUMENT_METADATA_DEFAULTS, +} from '../document-metadata.js'; + +describe('mergeDocumentMetadataDefaults', () => { + it('fills createdByClient and clientVersion when metadata is omitted', () => { + expect(mergeDocumentMetadataDefaults(NODE_SDK_DOCUMENT_METADATA_DEFAULTS)).toEqual({ + createdByClient: 'node-sdk', + clientVersion: VERSION, + }); + }); + + it('fills only missing keys when caller provides partial metadata', () => { + expect( + mergeDocumentMetadataDefaults(NODE_SDK_DOCUMENT_METADATA_DEFAULTS, { + title: 'Report.pdf', + }), + ).toEqual({ + createdByClient: 'node-sdk', + clientVersion: VERSION, + title: 'Report.pdf', + }); + }); + + it('lets caller overrides win for createdByClient and clientVersion', () => { + expect( + mergeDocumentMetadataDefaults(NODE_SDK_DOCUMENT_METADATA_DEFAULTS, { + createdByClient: 'cli', + clientVersion: '9.9.9', + title: 'Report.pdf', + }), + ).toEqual({ + createdByClient: 'cli', + clientVersion: '9.9.9', + title: 'Report.pdf', + }); + }); +}); diff --git a/src/lib/document-metadata.ts b/src/lib/document-metadata.ts new file mode 100644 index 0000000..91d24ea --- /dev/null +++ b/src/lib/document-metadata.ts @@ -0,0 +1,30 @@ +import type { DocumentMetadata } from '../types/params.js'; +import { VERSION } from '../version.js'; + +/** + * Official Knowhere client identity written into job `document_metadata`. + * + * Wire format (snake_case on the wire after HTTP serialization): + * `{ created_by_client, client_version }`. + * + * Merge rule used by every official client: caller-provided keys win; + * defaults only fill keys that are missing. + */ +export const NODE_SDK_DOCUMENT_METADATA_DEFAULTS = { + createdByClient: 'node-sdk', + clientVersion: VERSION, +} as const satisfies DocumentMetadata; + +/** + * Merge official-client defaults under caller-provided document metadata. + * Existing keys on `provided` are preserved; defaults fill gaps only. + */ +export function mergeDocumentMetadataDefaults( + defaults: DocumentMetadata, + provided?: DocumentMetadata | null, +): DocumentMetadata { + if (!provided) { + return { ...defaults }; + } + return { ...defaults, ...provided }; +} diff --git a/src/resources/__tests__/jobs.test.ts b/src/resources/__tests__/jobs.test.ts index 5564c89..62e3dd1 100644 --- a/src/resources/__tests__/jobs.test.ts +++ b/src/resources/__tests__/jobs.test.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Jobs } from '../jobs.js'; +import { VERSION } from '../../version.js'; import { InvalidStateError, NotFoundError } from '../../errors/index.js'; import type { HttpClient } from '../../lib/http-client.js'; import type { Job } from '../../types/job.js'; @@ -77,6 +78,10 @@ describe('Jobs Resource', () => { expect(mockHttpClient.post).toHaveBeenCalledWith('/v2/jobs', { sourceType: 'url', sourceUrl: 'https://example.com/doc.pdf', + documentMetadata: { + createdByClient: 'node-sdk', + clientVersion: VERSION, + }, }); }); @@ -230,11 +235,40 @@ describe('Jobs Resource', () => { expect.objectContaining({ documentMetadata: { createdByClient: 'notebook', + clientVersion: VERSION, title: 'Document.pdf', }, }), ); }); + + it('should let caller clientVersion override the SDK default', async () => { + mockHttpClient.post.mockResolvedValue({ + jobId: 'job-override', + status: 'pending', + sourceType: 'url', + createdAt: new Date(), + }); + + await jobs.create({ + sourceType: 'url', + sourceUrl: 'https://example.com/doc.pdf', + documentMetadata: { + createdByClient: 'cli', + clientVersion: '9.9.9', + }, + }); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/v2/jobs', + expect.objectContaining({ + documentMetadata: { + createdByClient: 'cli', + clientVersion: '9.9.9', + }, + }), + ); + }); }); describe('get', () => { diff --git a/src/resources/jobs.ts b/src/resources/jobs.ts index 2200325..cfe82c5 100644 --- a/src/resources/jobs.ts +++ b/src/resources/jobs.ts @@ -2,6 +2,10 @@ import { BaseResource } from './base.js'; import type { Job, JobResult } from '../types/job.js'; import type { CreateJobParams, UploadParams, WaitOptions, LoadOptions } from '../types/params.js'; import type { ParseResult } from '../types/result.js'; +import { + mergeDocumentMetadataDefaults, + NODE_SDK_DOCUMENT_METADATA_DEFAULTS, +} from '../lib/document-metadata.js'; import { uploadFile } from '../lib/upload.js'; import { pollJobStatus } from '../lib/polling.js'; import { parseResult } from '../lib/result-parser.js'; @@ -15,10 +19,20 @@ export class Jobs extends BaseResource { private pendingUploadJobs = new Map(); /** - * Create a new parsing job + * Create a new parsing job. + * + * Auto-attaches `documentMetadata.createdByClient` / `clientVersion` for + * OSS telemetry. Caller-provided metadata keys win; defaults fill gaps only. */ async create(params: CreateJobParams): Promise { - const job = await this.httpClient.post(this.endpoint('/jobs'), params); + const documentMetadata = mergeDocumentMetadataDefaults( + NODE_SDK_DOCUMENT_METADATA_DEFAULTS, + params.documentMetadata, + ); + const job = await this.httpClient.post(this.endpoint('/jobs'), { + ...params, + documentMetadata, + }); if (job.uploadUrl) { this.pendingUploadJobs.set(job.jobId, job); } diff --git a/src/version.ts b/src/version.ts index 37025ab..5515303 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.1.0'; +export const VERSION = '2.1.3';