Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/remove-sync-mcp-parse-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ontos-ai/knowhere-mcp": major
---

Remove the blocking `knowhere_parse_url` and `knowhere_parse_file` tools from the MCP server. Agents should use the async parse tools and poll `knowhere_async_get_job_status` before reading parsed results.
17 changes: 6 additions & 11 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ During login, the dashboard asks for a Permission:

- Read only: query Knowhere and read existing parsed documents. Parse and
delete tools are not exposed to the MCP host.
- Full access: query, read, parse URLs/files, cache completed parse jobs, and
archive documents.
- Full access: query, read, start async URL/file parse jobs, cache completed
parse jobs, and archive documents.

Useful auth commands:

Expand Down Expand Up @@ -178,12 +178,6 @@ When logged in with Read only permission, the MCP server exposes only
`knowhere_get_document_outline`, `knowhere_read_chunks`,
`knowhere_grep_chunks`, and `knowhere_async_get_job_status`.

- `knowhere_parse_url`: blocking parse for a remote URL; waits for completion
and makes the parsed document available to outline/read/grep/search tools.
- `knowhere_parse_file`: blocking parse for a file path available to the MCP
process; waits for completion and makes the parsed document available to
outline/read/grep/search tools. The path is resolved on the machine running
this stdio MCP server.
- `knowhere_async_parse_url`: start parsing a remote URL and return the job
immediately. When checking status, use exponential backoff.
- `knowhere_async_parse_file`: start parsing a local file path, upload it if
Expand Down Expand Up @@ -248,9 +242,10 @@ The following `<instruction>` tells callers to open or fetch the listed
`assetUrl` before relying on preview text. If a page asset exists without an
`assetUrl`, the text says that the asset is not directly readable.

Blocking parse responses summarize document/job identifiers, chunk counts, and
asset URL mappings. They intentionally do not dump the full parse result, ZIP
bytes, or every chunk.
Async parse responses return the job identifier and source metadata
immediately. Use `knowhere_async_get_job_status` to poll the job until it
completes, then pass the returned `localDocumentId`, `documentId`, or `jobId`
to outline, read, or grep tools.

## Package Boundary

Expand Down
74 changes: 16 additions & 58 deletions packages/mcp/src/__tests__/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ describe('knowhere MCP wrapper', () => {
const toolNames = tools.tools.map((tool) => tool.name).sort();
const statusTool = tools.tools.find((tool) => tool.name === 'knowhere_async_get_job_status');
const listTool = tools.tools.find((tool) => tool.name === 'knowhere_list_documents');
const parseFileTool = tools.tools.find((tool) => tool.name === 'knowhere_parse_file');
const asyncParseFileTool = tools.tools.find(
(tool) => tool.name === 'knowhere_async_parse_file',
);
Expand All @@ -32,8 +31,6 @@ describe('knowhere MCP wrapper', () => {
'knowhere_get_document_outline',
'knowhere_grep_chunks',
'knowhere_list_documents',
'knowhere_parse_file',
'knowhere_parse_url',
'knowhere_read_chunks',
'knowhere_search',
]);
Expand All @@ -43,9 +40,6 @@ describe('knowhere MCP wrapper', () => {
expect(statusTool?.description).toContain('5s, 10s, 20s, 40s, 80s');
expect(statusTool?.description).toContain('Large PDFs or OCR-heavy files can take 10+ minutes');
expect(listTool?.description).toContain('remote API');
expect(parseFileTool?.description).toContain(
'resolved on the machine running this stdio MCP server',
);
expect(asyncParseFileTool?.description).toContain(
'resolved on the machine running this stdio MCP server',
);
Expand Down Expand Up @@ -104,12 +98,12 @@ describe('knowhere MCP wrapper', () => {
await server.close();
});

it('should delegate parse and grep calls to the SDK knowledge module', async () => {
it('should delegate async parse and grep calls to the SDK knowledge module', async () => {
const knowhereClient = createClient();
const { client, server } = await connectTestClient(knowhereClient);

const parseResponse = await client.callTool({
name: 'knowhere_parse_file',
name: 'knowhere_async_parse_file',
arguments: {
file: './report.md',
localDocumentId: 'local-report',
Expand All @@ -127,7 +121,7 @@ describe('knowhere MCP wrapper', () => {
},
});

expect(knowhereClient.knowledge.parseToLocalCache).toHaveBeenCalledWith({
expect(knowhereClient.knowledge.startParse).toHaveBeenCalledWith({
file: './report.md',
fileName: undefined,
namespace: undefined,
Expand All @@ -149,16 +143,8 @@ describe('knowhere MCP wrapper', () => {
});
expectToolText(
parseResponse,
`<knowhere operation="parseFile">
<document localDocumentId="local-report" documentId="doc-1" jobId="job-1" namespace="support-center" sourceFileName="report.md" storageRoot="/tmp/knowhere/local-report">
<chunkCounts total="3" text="1" image="0" table="0" page="2" />
</document>
<parseResult jobId="job-1" documentId="doc-1" namespace="support-center" sourceFileName="report.md">
<chunkCounts total="3" text="1" image="0" table="0" page="2" />
</parseResult>
<assetUrls count="1">
<assetUrl filePath="page_citation_assets/page-1.png" assetUrl="https://assets.example/page-1.png" />
</assetUrls>
`<knowhere operation="asyncParseFile">
<job localDocumentId="local-report" jobId="job-async" status="running" sourceType="file" documentId="doc-async" namespace="support-center" />
</knowhere>`,
);
expectToolText(
Expand Down Expand Up @@ -807,44 +793,18 @@ function createClient(): Knowhere & {
knowledge: KnowledgeWithMocks;
} {
const knowledge: KnowledgeWithMocks = {
parseToLocalCache: vi.fn().mockResolvedValue({
document: createLocalDocument(),
result: {
jobId: 'job-1',
documentId: 'doc-1',
namespace: 'support-center',
manifest: {
jobId: 'job-1',
sourceFileName: 'report.md',
statistics: {
totalChunks: 3,
textChunks: 1,
imageChunks: 0,
tableChunks: 0,
pageChunks: 2,
},
startParse: vi.fn().mockImplementation((params: Parameters<Knowledge['startParse']>[0]) =>
Promise.resolve({
job: {
jobId: 'job-async',
status: 'running',
sourceType: 'file' in params ? 'file' : 'url',
documentId: 'doc-async',
namespace: 'support-center',
},
rawZip: Buffer.from('not rendered'),
chunks: [
{
chunkId: 'not-rendered',
},
],
},
assetUrlsByFilePath: {
'page_citation_assets/page-1.png': 'https://assets.example/page-1.png',
},
}),
startParse: vi.fn().mockResolvedValue({
job: {
jobId: 'job-async',
status: 'running',
sourceType: 'url',
documentId: 'doc-async',
namespace: 'support-center',
},
localDocumentId: 'local-report',
}),
localDocumentId: 'local-report',
}),
),
getJobStatus: vi.fn().mockResolvedValue({
job: {
jobId: 'job-async',
Expand Down Expand Up @@ -996,7 +956,6 @@ type ToolCallResponse = Awaited<ReturnType<Client['callTool']>>;

type KnowledgeWithMocks = Pick<
Knowledge,
| 'parseToLocalCache'
| 'startParse'
| 'getJobStatus'
| 'importJobResult'
Expand All @@ -1009,7 +968,6 @@ type KnowledgeWithMocks = Pick<
| 'withCacheDirectory'
| 'withParsedStorage'
> & {
parseToLocalCache: Mock<Knowledge['parseToLocalCache']>;
startParse: Mock<Knowledge['startParse']>;
getJobStatus: Mock<Knowledge['getJobStatus']>;
importJobResult: Mock<Knowledge['importJobResult']>;
Expand Down
54 changes: 0 additions & 54 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,60 +62,6 @@ export async function createKnowhereMcpServer(
const hasWritePermission = permission === 'full_access';

if (hasWritePermission) {
server.registerTool(
'knowhere_parse_url',
{
description:
'Blocking parse: submit a remote URL to Knowhere, wait for completion, then make the parsed document available to outline/read/grep/search tools.',
inputSchema: {
url: z.string().url(),
namespace: z.string().optional(),
localDocumentId: z.string().optional(),
dataId: z.string().optional(),
parsingParams: parsingParamsSchema,
},
},
async (input) =>
createToolResult(
'parseUrl',
await knowledge.parseToLocalCache({
url: input.url,
namespace: input.namespace,
localDocumentId: input.localDocumentId,
dataId: input.dataId,
...toFlatParsingParams(input.parsingParams),
}),
),
);

server.registerTool(
'knowhere_parse_file',
{
description:
'Blocking parse: submit a local file path available to this MCP process, wait for completion, then make the parsed document available to outline/read/grep/search tools. The file path is resolved on the machine running this stdio MCP server, not on a remote chat client.',
inputSchema: {
file: z.string().describe('Local file path available to this MCP server process.'),
fileName: z.string().optional(),
namespace: z.string().optional(),
localDocumentId: z.string().optional(),
dataId: z.string().optional(),
parsingParams: parsingParamsSchema,
},
},
async (input) =>
createToolResult(
'parseFile',
await knowledge.parseToLocalCache({
file: input.file,
fileName: input.fileName,
namespace: input.namespace,
localDocumentId: input.localDocumentId,
dataId: input.dataId,
...toFlatParsingParams(input.parsingParams),
}),
),
);

server.registerTool(
'knowhere_async_parse_url',
{
Expand Down
59 changes: 0 additions & 59 deletions packages/mcp/src/tool-result-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ function formatOperationResult(operation: string, result: unknown): string {
const lines: string[] = [`<knowhere operation="${escapeAttribute(operation)}">`];

switch (operation) {
case 'parseUrl':
case 'parseFile':
appendBlockingParseResult(lines, result);
break;
case 'asyncParseUrl':
case 'asyncParseFile':
appendAsyncParseResult(lines, result);
Expand Down Expand Up @@ -92,13 +88,6 @@ function formatOperationResult(operation: string, result: unknown): string {
return lines.join('\n');
}

function appendBlockingParseResult(lines: string[], result: unknown): void {
const response: UnknownRecord | undefined = toRecord(result);
appendDocument(lines, readRecord(response, 'document'), 1);
appendParseSummary(lines, readRecord(response, 'result'), 1);
appendAssetUrls(lines, readRecord(response, 'assetUrlsByFilePath'), 1);
}

function appendAsyncParseResult(lines: string[], result: unknown): void {
const response: UnknownRecord | undefined = toRecord(result);
const job: UnknownRecord | undefined = readRecord(response, 'job');
Expand Down Expand Up @@ -256,54 +245,6 @@ function appendSearchResult(lines: string[], result: unknown): void {
lines.push(`${indent(1)}</search>`);
}

function appendParseSummary(
lines: string[],
result: UnknownRecord | undefined,
depth: number,
): void {
const manifest: UnknownRecord | undefined = readRecord(result, 'manifest');
const statistics: UnknownRecord | undefined = readRecord(manifest, 'statistics');
lines.push(
`${indent(depth)}<parseResult${formatAttributes({
jobId: readString(result, 'jobId') ?? readString(manifest, 'jobId'),
documentId: readString(result, 'documentId'),
namespace: readString(result, 'namespace'),
sourceFileName: readString(manifest, 'sourceFileName'),
})}>`,
);
appendChunkCounts(lines, statistics, depth + 1, readNumber(statistics, 'totalChunks'));
lines.push(`${indent(depth)}</parseResult>`);
}

function appendAssetUrls(
lines: string[],
assetUrlsByFilePath: UnknownRecord | undefined,
depth: number,
): void {
if (!assetUrlsByFilePath) {
return;
}

const entries: readonly [string, string][] = Object.entries(assetUrlsByFilePath).filter(
(entry): entry is [string, string] => typeof entry[1] === 'string',
);
if (entries.length === 0) {
return;
}

lines.push(`${indent(depth)}<assetUrls${formatAttributes({ count: entries.length })}>`);
for (const [filePath, assetUrl] of entries) {
appendSelfClosingTag(lines, depth + 1, {
name: 'assetUrl',
attributes: {
filePath,
assetUrl,
},
});
}
lines.push(`${indent(depth)}</assetUrls>`);
}

function appendDocument(lines: string[], document: UnknownRecord | undefined, depth: number): void {
if (!document) {
appendSelfClosingTag(lines, depth, { name: 'document' });
Expand Down
Loading