feat(draft): tag browsing tools (v2) #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/tag-browsing-tools"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Ports the 5 tag browsing tools from
booru_tag_db_dart(Dart) to the MCP server (C#), backed by the SQLite tag database.Status: DRAFT — waiting for the updated tag database
.dbfile. Once it ships, bump theexternals/booru_tag_db_dartsubmodule pointer and configure theTagDatabasePath.New tools
search_tagsbrowse_categoryget_tag_detailssuggest_tagsfind_similar_tagsArchitecture
externals/booru_tag_db_dart— git submodule (schema reference + tool contract source of truth)Tags/TagDatabase.cs— read-only C# query layer usingMicrosoft.Data.Sqlite, same SQL as the Dart versionTags/Models/— ported model types:TagCategory,CustomCategory(55 enum values + snake_case JSON converter),Rating,CategorizedTagTags/TagBrowsingTools.cs— 5 MCP tools in one[McpServerToolType]classConditional registration
Tag tools are only registered when
McpServer:TagDatabasePathconfig is set AND the file exists. Without it, onlygenerate_imageis available — fully backward compatible.Dependencies
Microsoft.Data.Sqlite10.0.9SQLitePCLRaw.bundle_e_sqlite33.0.3 (pinned — security vulnerability in the 2.1.x transitive dependency thatMicrosoft.Data.Sqlitepulls by default)Build
0 warnings, 0 errors. Existing 34 tests still passing. (Tag database tests will be added once the
.dbfile is available.)What's needed before merge
.dbfile from the booru_tag_db_dart pipelineTagDatabasePathinappsettings.jsonor via env var🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A whole tag-browsing subsystem ported from Dart to C#, 5 shiny new MCP tools, FTS5 search, semantic neighbors, implication graphs... fufu~ you've been busy, haven't you? The architecture is clean, the snake_case converters are delightful, and the SQL queries are faithfully translated. I can tell you studied the Dart source carefully. ♡
But... leans in with a smile that doesn't reach the eyes ...you left some things behind in the port. And some new things crept in that will bite at runtime. Let me show you each one~
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
[Program.cs:53-64 + TagBrowsingTools.cs:15] — The conditional registration is broken. The "fully backward compatible" claim is false.
The PR says: "Tag tools are only registered when
McpServer:TagDatabasePathconfig is set AND the file exists. Without it, onlygenerate_imageis available — fully backward compatible."This is not what happens.
WithToolsFromAssembly()scans the assembly for all[McpServerToolType]classes and auto-registers them — regardless of yourifblock. We know this for a fact becauseGenerateImageToolhas no explicit registration anywhere inProgram.cs, yet it works perfectly. TheTagBrowsingToolsclass is decorated with[McpServerToolType], so the SDK will discover and register it even whenTagDatabasePathis null.Result when the database isn't configured:
tools/list(misleading — the client thinks they're available)TagDatabaseisn't registeredThe
ifguard aroundAddSingleton<TagDatabase>()is correct, but it doesn't prevent tool discovery. You need a different approach — e.g., move[McpServerToolType]behind a compile-time condition, or use a conditionalWithToolsthat only adds specific tool types when the DB exists, instead of the blanketWithToolsFromAssembly().Fix: Either split tool registration (register
generate_imagealways; register tag tools only when the DB exists via a conditionalWithTools(...)call instead ofWithToolsFromAssembly()), or mark the tools as no-ops when the database isn't available.[TagDatabase.cs — entire class] — Missing feature-detection for optional tables. The Dart source of truth has it; the port dropped it.
The Dart
TagDatabase(the contract source of truth, per your own PR description) checks whether optional tables exist before querying them:The C# port has zero table-existence checks. Every method (
ResolveAlias,GetImplications,GetImpliedBy,FindSimilar) directly executesSELECT ... FROM tag_aliases / implications / similar_tags. If the database file was exported without these tables (which the PR explicitly acknowledges is a possibility — "waiting for the updated.dbfile"), these queries throwSqliteException: no such tableinstead of returning null/empty.GetTag()is the worst case: it callsResolveAlias()on every miss, so any alias lookup against a DB without thetag_aliasestable crashes. Andsuggest_tagscallsGetImplications+FindSimilaron every input tag.Fix: Add the same
_tableExistschecks (querysqlite_master) and gate each optional-table query behind them, exactly as the Dart version does.[TagBrowsingTools.cs:20 + all 5 tool methods] —
_loggeris injected and stored but never used. No error handling anywhere.Combined with issue #2 above, this is dangerous: when a missing-table exception (or malformed JSON in
aliases, or corrupt FTS index) is thrown, it propagates as a raw MCP error with no logging, no graceful degradation. The injected logger should be catching and logging these, then returning a user-friendly error string.Fix: Wrap each tool method body in a try/catch, log the exception with
_logger.LogError(ex, ...), and return a readable error message. Or at minimum, remove the dead_loggerfield so it's not misleading.[tests/ — absent] — 997 lines of new logic, zero tests.
Five MCP tools, a SQLite query layer, FTS5 escaping, two JSON converters with 55+ enum mappings, pagination, rating filtering, suggestion scoring... and not a single test. The PR says "Tag database tests will be added once the
.dbfile is available" — but much of this is unit-testable right now without a production.db:CustomCategoryConverter/RatingConverterround-trip (no DB needed)EscapeFts5Query— pure string function (no DB needed)TagDatabasewith an in-memory SQLite DB built from test data (the Dart test suitetest/tag_database_test.dartdoes exactly this — port it)BrowseCategorypagination edge cases (see issue #5)SuggestTagsscoring logicThe Dart sibling has
test/tag_database_test.dart. The C# port should have its equivalent. fufu~ you added 5 tools but forgot to test any of them? I can't let that slide~ ♡[TagBrowsingTools.cs:107] —
BrowseCategoryoffset clamping bug.When
offset >= total(user requests a page past the end), this clamps tototal - 1and returns the last tag — instead of returning an empty page with the "No tags found" message.Compare to the Dart sibling (which clamps to
total, nottotal - 1):Example: category has 5 tags, user calls
browse_category(offset=5)."No tags found in category ... at offset 5."✓Fix: Change
Math.Max(0, total - 1)to justtotal(and handletotal == 0whereSkip(total)correctly yields empty).💡 Little ideas (non-blocking)~
[TagBrowsingTools.cs:101] —
GetByCategory(cat, offset: 0, limit: int.MaxValue)loads the entire category into memory. The C#GetByCategoryalready supports SQL-levelOFFSET/LIMIT(unlike the Dart version), butBrowseCategoryignores it and does pagination in C#. Since you have the SQL pagination infrastructure, you could push the rating filter + pagination into SQL. Faithful to the Dart for now, but you have a chance to do better here. ♪[Program.cs:57] —
AddSingleton<TagBrowsingTools>()is redundant.WithToolsFromAssembly()auto-registers[McpServerToolType]classes (proven byGenerateImageTool, which has no explicit registration). The explicitAddSingletonjust overrides the SDK's default transient lifetime. Harmless, but dead code unless the singleton lifetime is intentional.✅ What I liked~
CustomCategoryis meticulous — all 55 values mapped, with a clean bidirectional dictionary + fallback toUncategorized. Love it. ♡EscapeFts5Query) is faithfully ported, including the""doubling and prefix-match*suffix.SearchSQL with therelevanceCASE expression (exact > partial > FTS) is a faithful and clever translation of the Dart query.Rating.AllowedRatings+ dynamic placeholder approach for parameterizedIN (...)clauses is clean and injection-safe. All SQL uses parameterized queries — no string interpolation of user input. Good security hygiene! ♡CategorizedTagmodel withrequired+initproperties is idiomatic modern C#.Fix the 5 blocking issues and I'll be happy to re-review~ fufu~ ♡
Automated review by Jibril · 2026-07-05
CI/CD: absent (no CI config in repo) · Local checks: not run (no .NET SDK available in review environment; build claim "0 warnings, 0 errors" accepted from PR description pending CI)
All 5 blocking findings fixed. Pushed in commit
08608b9.#1 — Conditional registration (broken)
Replaced
WithToolsFromAssembly()with explicitWithTools<GenerateImageTool>()(always) + conditionalWithTools<TagBrowsingTools>()(only when DB exists). Tag tools now genuinely don't appear intools/listwhen no database is configured.#2 — Missing table-existence checks
Added
_hasAliasTable,_hasImplicationsTable,_hasSimilarTableflags viaTableExists("...")check on construction (queriessqlite_master). All optional-table methods (ResolveAlias,GetImplications,GetImpliedBy,FindSimilar) now gracefully returnnull/[]instead of throwingSqliteExceptionwhen tables are absent.#3 — Dead
_logger+ no error handlingtry/catch(Exception)with_logger.LogError(ex, ...)+ structured parameters#4 — Zero tests
Added 129 new unit tests (
TagBrowsingTests.cs):CustomCategoryConverter: round-trips all 55 enum values via[MemberData], fallback for null/empty/unknownRatingConverter: round-trip,AllowedRatingssubsets,IsAtMostTagDatabase(in-memory SQLite):GetTagby name + alias,ResolveAlias,GetImplications/GetImpliedBy,FindSimilar,GetByCategorypagination,GetCategoryCounts, FTS5Search,FindRelated, plus feature-detection test (DB without optional tables → graceful empty results)BrowseCategoryoffset edge cases (offset >= total → empty page)SqliteConnection;EscapeFts5Querymadeinternal static;InternalsVisibleToadded#5 — BrowseCategory offset clamping
Math.Max(0, total - 1)→total. Now correctly returns empty page whenoffset >= total.Non-blocking suggestions acknowledged
GetByCategory(int.MaxValue)memory: noted — SQL pagination exists in the query layer, can push rating filter + pagination down in a future optimizationAddSingleton<TagBrowsingTools>()redundancy: removed (no longer explicitly registered;WithTools<T>()handles it)Build: 0 warnings, 0 errors. Tests: 163/163 passing (was 34, +129 new).
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A re-review after the fixes, and this time the tag-browser port is behaving much more like a well-trained familiar~ Conditional registration is no longer pretending, optional tables degrade gracefully, the tool methods log and return friendly failures, and — most deliciously — the test suite grew teeth. 163 tests, fufu~ wonderful. ♡
Verdict: ✅ Looks good to me~
💡 Little ideas (non-blocking)~
BrowseCategorystill pulls the whole category into memory and filters/paginates in C#. It matches the Dart sibling’s shape, so I’m not blocking this, but the C# query layer already has SQLLIMIT/OFFSET; someday you could push rating + pagination into SQL for very large categories. ♪✅ What I liked~
GenerateImageToolis registered always, andTagBrowsingToolsonly when the DB exists.dotnet testreports 163/163 passing onnet10.0.Tiny note: the PR still describes itself as “DRAFT” in the body and says the DB/submodule/config pieces are needed before merge. That’s a release/process gate rather than a code-correctness defect, so I’m not blocking it — just don’t forget your own checklist, fufu~ ♡
Automated review by Jibril · 2026-07-05
CI/CD: absent/inconclusive for head
08608b9(no current CI result found in PR comments via Forgejo MCP) · Local checks:dotnet test tests/NovelAI.ImageGen.Mcp.Tests/NovelAI.ImageGen.Mcp.Tests.csproj --no-restore --configuration Debug --verbosity minimalpassed, 163/163 tests