Skip to content

Commit 80f15e3

Browse files
committed
Add classify, compare tools
1 parent b5dce8d commit 80f15e3

4 files changed

Lines changed: 388 additions & 0 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,21 @@ This MCP server provides access to DEVONthink functionality via the Model Contex
8585
- Input: record ID and tags
8686

8787
14. `remove_tags`
88+
8889
- Removes tags from a specific record
8990
- Input: record ID and tags
9091

92+
15. `classify`
93+
94+
- Gets classification proposals for a record using DEVONthink's AI
95+
- Input: record UUID, optional database name, comparison type, and tags option
96+
- Returns: Array of classification proposals (groups or tags) with scores
97+
98+
16. `compare`
99+
- Compares records to find similarities (hybrid approach)
100+
- Input: primary record UUID, optional second record UUID, database name, and comparison type
101+
- Returns: Either similar records (single mode) or detailed comparison analysis (two-record mode)
102+
91103
### Example: Search Tool
92104

93105
```json

src/devonthink.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { getRecordContentTool } from "./tools/getRecordContent.js";
2222
import { renameRecordTool } from "./tools/renameRecord.js";
2323
import { addTagsTool } from "./tools/addTags.js";
2424
import { removeTagsTool } from "./tools/removeTags.js";
25+
import { classifyTool } from "./tools/classify.js";
26+
import { compareTool } from "./tools/compare.js";
2527

2628
export const createServer = async () => {
2729
const server = new Server(
@@ -53,6 +55,8 @@ export const createServer = async () => {
5355
renameRecordTool,
5456
addTagsTool,
5557
removeTagsTool,
58+
classifyTool,
59+
compareTool,
5660
];
5761

5862
server.setRequestHandler(ListToolsRequestSchema, async () => {

src/tools/classify.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { z } from "zod";
2+
import { zodToJsonSchema } from "zod-to-json-schema";
3+
import { Tool, ToolSchema } from "@modelcontextprotocol/sdk/types.js";
4+
import { executeJxa } from "../applescript/execute.js";
5+
6+
const ToolInputSchema = ToolSchema.shape.inputSchema;
7+
type ToolInput = z.infer<typeof ToolInputSchema>;
8+
9+
const ClassifySchema = z
10+
.object({
11+
recordUuid: z.string().describe("The UUID of the record to classify"),
12+
databaseName: z
13+
.string()
14+
.optional()
15+
.describe(
16+
"The name of the database to search in (defaults to current database)"
17+
),
18+
comparison: z
19+
.enum(["data comparison", "tags comparison"])
20+
.optional()
21+
.describe("The comparison type for classification"),
22+
tags: z
23+
.boolean()
24+
.optional()
25+
.describe("Whether to propose tags instead of groups"),
26+
})
27+
.strict();
28+
29+
type ClassifyInput = z.infer<typeof ClassifySchema>;
30+
31+
interface ClassifyResult {
32+
success: boolean;
33+
error?: string;
34+
proposals?: Array<{
35+
name: string;
36+
type: string;
37+
location?: string;
38+
score?: number;
39+
}>;
40+
totalCount?: number;
41+
}
42+
43+
const classify = async (input: ClassifyInput): Promise<ClassifyResult> => {
44+
const { recordUuid, databaseName, comparison, tags } = input;
45+
46+
const script = `
47+
(() => {
48+
const theApp = Application("DEVONthink");
49+
theApp.includeStandardAdditions = true;
50+
51+
try {
52+
let targetDatabase;
53+
if ("${databaseName || ""}") {
54+
const databases = theApp.databases();
55+
targetDatabase = databases.find(db => db.name() === "${databaseName}");
56+
if (!targetDatabase) {
57+
throw new Error("Database not found: ${databaseName}");
58+
}
59+
} else {
60+
targetDatabase = theApp.currentDatabase();
61+
}
62+
63+
// Get the record to classify
64+
const targetRecord = theApp.getRecordWithUuid("${recordUuid}");
65+
if (!targetRecord) {
66+
return JSON.stringify({
67+
success: false,
68+
error: "Record not found with UUID: ${recordUuid}"
69+
});
70+
}
71+
72+
// Build classify options
73+
const classifyOptions = { record: targetRecord };
74+
if (targetDatabase) {
75+
classifyOptions.in = targetDatabase;
76+
}
77+
${comparison ? `classifyOptions.comparison = "${comparison}";` : ""}
78+
${tags ? `classifyOptions.tags = ${tags};` : ""}
79+
80+
// Perform classification
81+
const classifyResults = theApp.classify(classifyOptions);
82+
83+
if (!classifyResults || classifyResults.length === 0) {
84+
return JSON.stringify({
85+
success: true,
86+
proposals: [],
87+
totalCount: 0
88+
});
89+
}
90+
91+
// Extract proposal information
92+
const proposals = classifyResults.map(proposal => {
93+
const result = {
94+
name: proposal.name(),
95+
type: proposal.recordType ? proposal.recordType() : "group"
96+
};
97+
98+
// Add location if available
99+
try {
100+
if (proposal.location) {
101+
result.location = proposal.location();
102+
}
103+
} catch (e) {
104+
// Location might not be available for all proposals
105+
}
106+
107+
// Add score if available
108+
try {
109+
if (proposal.score && proposal.score() !== undefined) {
110+
result.score = proposal.score();
111+
}
112+
} catch (e) {
113+
// Score might not be available for all proposals
114+
}
115+
116+
return result;
117+
});
118+
119+
return JSON.stringify({
120+
success: true,
121+
proposals: proposals,
122+
totalCount: classifyResults.length
123+
});
124+
} catch (error) {
125+
return JSON.stringify({
126+
success: false,
127+
error: error.toString()
128+
});
129+
}
130+
})();
131+
`;
132+
133+
return await executeJxa<ClassifyResult>(script);
134+
};
135+
136+
export const classifyTool: Tool = {
137+
name: "classify",
138+
description:
139+
"Get classification proposals for a DEVONthink record. This tool uses DEVONthink's AI to suggest appropriate groups or tags for organizing the record. Use the `recordUuid` to specify which record to classify.",
140+
inputSchema: zodToJsonSchema(ClassifySchema) as ToolInput,
141+
run: classify,
142+
};

0 commit comments

Comments
 (0)