mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
fix(mcp): rank list_tags by use and let it narrow, instead of every tag at once
A real library of 488 models has 4,306 tags: the whole list came to 63 KB, more of an assistant's context than one answer should take. It now returns the most used 200 by default, with how many models have each, and takes a query to narrow them.
This commit is contained in:
1 parent
7a31ed48e2
commit
04e97197d7
5 files changed
+42
-11
No files matched your search
@@ -6,7 +6,7 @@ import { HttpError } from "../utils/fileUtils";
|
||||
import { SYSTEM_COLLECTIONS, systemCollectionKeyForId } from "../services/collectionService";
|
||||
import { getDashboardSummary } from "../services/dashboardService";
|
||||
import { printOutById } from "../services/printLoader";
|
||||
import { listPrints, listTagsOf, PRINT_SORT_MODES, SELF_AUTHOR_ID } from "../services/printQuery";
|
||||
import { listPrints, PRINT_SORT_MODES, SELF_AUTHOR_ID } from "../services/printQuery";
|
||||
import { search } from "../services/searchService";
|
||||
import { addModelsToCollection, createCollection, isSystemCollectionId } from "../services/collectionService";
|
||||
import { getImportTask, startImportTask, type ImportTaskState } from "../services/importTaskService";
|
||||
@@ -235,11 +235,30 @@ const getCollection: McpToolDefinition = {
|
||||
const listTags: McpToolDefinition = {
|
||||
name: "list_tags",
|
||||
title: "List tags",
|
||||
description: "Every tag in the library, so models can then be listed by the exact tag names it uses.",
|
||||
description:
|
||||
"The library's tags, most used first, with how many models have each, so models can then be listed by the exact " +
|
||||
"tag names it uses. A big library has thousands: narrow them with query.",
|
||||
readOnly: true,
|
||||
inputSchema: {},
|
||||
async run({ userId }) {
|
||||
return { tags: await listTagsOf(userId, {}) };
|
||||
inputSchema: {
|
||||
query: z.string().optional().describe("Only tags containing this, ignoring case, e.g. 'drag' for 'dragon'."),
|
||||
limit: z.number().int().min(1).max(500).optional().describe("How many to return, up to 500 (default 200)."),
|
||||
},
|
||||
async run({ userId }, args) {
|
||||
const query = ((args.query as string | undefined) ?? "").trim().toLowerCase();
|
||||
const rows = await prisma.print.findMany({ where: { userId }, select: { tags: true } });
|
||||
// Counted case-insensitively, under the spelling seen first, as the library's tag list does.
|
||||
const counts = new Map<string, { tag: string; model_count: number }>();
|
||||
for (const row of rows) {
|
||||
for (const tag of new Set(row.tags.map((t) => t.trim()).filter(Boolean))) {
|
||||
const key = tag.toLowerCase();
|
||||
if (query && !key.includes(query)) continue;
|
||||
const entry = counts.get(key);
|
||||
if (entry) entry.model_count++;
|
||||
else counts.set(key, { tag, model_count: 1 });
|
||||
}
|
||||
}
|
||||
const all = [...counts.values()].toSorted((a, b) => b.model_count - a.model_count || a.tag.localeCompare(b.tag));
|
||||
return { tags: all.slice(0, (args.limit as number | undefined) ?? 200), total: all.length };
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -314,11 +314,23 @@ describe("the tools", () => {
|
||||
expect(other.isError).toBe(true);
|
||||
});
|
||||
|
||||
it("list_tags lists only the user's tags", async () => {
|
||||
it("list_tags lists only the user's tags, most used first, and narrows by query", async () => {
|
||||
const { data } = await callTool(alice.token, "list_tags");
|
||||
expect(data.tags.map((t: string) => t.toLowerCase())).toEqual(
|
||||
expect.arrayContaining(["dragon", "flexi", "calibration"]),
|
||||
const byTag = new Map(
|
||||
data.tags.map((t: { tag: string; model_count: number }) => [t.tag.toLowerCase(), t.model_count]),
|
||||
);
|
||||
expect(byTag.get("dragon")).toBe(1);
|
||||
expect(byTag.get("calibration")).toBe(1);
|
||||
expect(data.total).toBe(3);
|
||||
|
||||
const narrowed = await callTool(alice.token, "list_tags", { query: "DRAG", limit: 5 });
|
||||
expect(narrowed.data.tags.map((t: { tag: string }) => t.tag.toLowerCase())).toEqual(["dragon"]);
|
||||
// Bob's "dragon" model doesn't count towards Alice's tags.
|
||||
expect(narrowed.data.tags[0].model_count).toBe(1);
|
||||
|
||||
const one = await callTool(alice.token, "list_tags", { limit: 1 });
|
||||
expect(one.data.tags).toHaveLength(1);
|
||||
expect(one.data.total).toBe(3);
|
||||
});
|
||||
|
||||
it("list_categories and library_stats answer for the user", async () => {
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ These read your library, and are on for a new token:
|
||||
| `get_model` | One model's description, files, tags, category, author, collections and where it was imported from. |
|
||||
| `list_collections` | Your collections and how many models each holds, plus Favourites and Browsing History. |
|
||||
| `get_collection` | The models in one collection. |
|
||||
| `list_tags` | Every tag in your library. |
|
||||
| `list_tags` | Your tags, most used first, narrowed by a word if you like. |
|
||||
| `list_categories` | Your category and folder tree. |
|
||||
| `get_author` | A designer, where to find them, and their models in your library. |
|
||||
| `library_stats` | Your totals, and your most viewed, most printed and newest models. |
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
},
|
||||
"list_tags": {
|
||||
"title": "List tags",
|
||||
"description": "See every tag in your library."
|
||||
"description": "See your tags, most used first."
|
||||
},
|
||||
"list_categories": {
|
||||
"title": "List categories",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
},
|
||||
"list_tags": {
|
||||
"title": "Žymių sąrašas",
|
||||
"description": "Matyti visas bibliotekos žymes."
|
||||
"description": "Matyti savo žymes, dažniausiai naudojamas pirmiau."
|
||||
},
|
||||
"list_categories": {
|
||||
"title": "Kategorijų sąrašas",
|
||||
|
||||
Reference in new issue
Block a user