enterprise-architect-mcp 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +226 -0
  3. package/dist/database.d.ts +3 -0
  4. package/dist/database.js +25 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +68 -0
  7. package/dist/model-session.d.ts +29 -0
  8. package/dist/model-session.js +172 -0
  9. package/dist/package-path.d.ts +2 -0
  10. package/dist/package-path.js +36 -0
  11. package/dist/remembered-path.d.ts +5 -0
  12. package/dist/remembered-path.js +43 -0
  13. package/dist/resolve-qea-path.d.ts +26 -0
  14. package/dist/resolve-qea-path.js +58 -0
  15. package/dist/text.d.ts +37 -0
  16. package/dist/text.js +152 -0
  17. package/dist/tools/annotations.d.ts +3 -0
  18. package/dist/tools/annotations.js +5 -0
  19. package/dist/tools/connectors.d.ts +3 -0
  20. package/dist/tools/connectors.js +152 -0
  21. package/dist/tools/diagrams.d.ts +3 -0
  22. package/dist/tools/diagrams.js +225 -0
  23. package/dist/tools/elements.d.ts +3 -0
  24. package/dist/tools/elements.js +210 -0
  25. package/dist/tools/packages.d.ts +3 -0
  26. package/dist/tools/packages.js +80 -0
  27. package/dist/tools/resolve.d.ts +3 -0
  28. package/dist/tools/resolve.js +135 -0
  29. package/dist/tools/scenarios.d.ts +3 -0
  30. package/dist/tools/scenarios.js +106 -0
  31. package/dist/tools/schema.d.ts +3 -0
  32. package/dist/tools/schema.js +164 -0
  33. package/dist/tools/search.d.ts +3 -0
  34. package/dist/tools/search.js +217 -0
  35. package/dist/tools/windowing.d.ts +37 -0
  36. package/dist/tools/windowing.js +92 -0
  37. package/dist/tools.d.ts +3 -0
  38. package/dist/tools.js +18 -0
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +1 -0
  41. package/package.json +68 -0
@@ -0,0 +1,80 @@
1
+ import { READ_ONLY } from "./annotations.js";
2
+ import { z } from "zod";
3
+ const MAX_PACKAGES = 200;
4
+ export function configurePackageTools(server, model) {
5
+ server.tool("ea_get_package_tree", "Navigate the package hierarchy. Without parameters, returns top-level `packages`. With a `packageId`, returns that package's children up to the specified depth. Each node carries `id`, `name`, `parentId`, and `elementCount`.", {
6
+ packageId: z.coerce.number().optional().describe("Package ID to get children of. Omit for top-level packages."),
7
+ depth: z.coerce.number().default(1).describe("How many levels deep to recurse (max 3, default 1)"),
8
+ }, READ_ONLY, async ({ packageId, depth }) => {
9
+ const db = await model.database();
10
+ try {
11
+ const effectiveDepth = Math.min(depth, 3);
12
+ const parentId = packageId ?? 0;
13
+ // Verify package exists when a specific ID is requested
14
+ if (packageId != null && packageId !== 0) {
15
+ const pkgExists = db.prepare("SELECT Package_ID FROM t_package WHERE Package_ID = ?").get(packageId);
16
+ if (!pkgExists) {
17
+ return {
18
+ content: [{ type: "text", text: JSON.stringify({ error: "not_found", message: `Package with ID ${packageId} not found`, packageId }, null, 2) }],
19
+ isError: true,
20
+ };
21
+ }
22
+ }
23
+ let totalCount = 0;
24
+ function getChildren(pid, currentDepth) {
25
+ if (currentDepth <= 0 || totalCount >= MAX_PACKAGES)
26
+ return [];
27
+ const packages = db.prepare(`
28
+ SELECT p.Package_ID, p.Name, p.Parent_ID
29
+ FROM t_package p
30
+ WHERE p.Parent_ID = ?
31
+ ORDER BY p.TPos, p.Name
32
+ `).all(pid);
33
+ const result = [];
34
+ for (const pkg of packages) {
35
+ if (totalCount >= MAX_PACKAGES)
36
+ break;
37
+ totalCount++;
38
+ const countRow = db.prepare("SELECT COUNT(*) as cnt FROM t_object WHERE Package_ID = ?").get(pkg.Package_ID);
39
+ const node = {
40
+ id: pkg.Package_ID,
41
+ name: pkg.Name,
42
+ parentId: pkg.Parent_ID,
43
+ elementCount: countRow.cnt,
44
+ };
45
+ if (currentDepth > 1) {
46
+ const children = getChildren(pkg.Package_ID, currentDepth - 1);
47
+ if (children.length > 0) {
48
+ node.children = children;
49
+ }
50
+ }
51
+ result.push(node);
52
+ }
53
+ return result;
54
+ }
55
+ const tree = getChildren(parentId, effectiveDepth);
56
+ const truncated = totalCount >= MAX_PACKAGES;
57
+ const response = {
58
+ packages: tree,
59
+ totalMatched: totalCount,
60
+ returned: tree.length,
61
+ truncated,
62
+ _meta: { sourceTables: ["t_package", "t_object"] },
63
+ };
64
+ if (truncated) {
65
+ response.message = `Results truncated at ${MAX_PACKAGES} packages. Use a specific packageId to drill deeper.`;
66
+ response.continuation = { tool: "ea_get_package_tree", arguments: { packageId: parentId, depth: effectiveDepth } };
67
+ }
68
+ return {
69
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
70
+ };
71
+ }
72
+ catch (error) {
73
+ const msg = error instanceof Error ? error.message : String(error);
74
+ return {
75
+ content: [{ type: "text", text: `Error retrieving package tree: ${msg}` }],
76
+ isError: true,
77
+ };
78
+ }
79
+ });
80
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ModelAccess } from "../model-session.js";
3
+ export declare function configureResolveTools(server: McpServer, model: ModelAccess): void;
@@ -0,0 +1,135 @@
1
+ import { READ_ONLY } from "./annotations.js";
2
+ import { z } from "zod";
3
+ import { buildPackagePath } from "../package-path.js";
4
+ import { foldText } from "../text.js";
5
+ function isBracedGuid(ref) {
6
+ return /^\{[^}]+\}$/.test(ref.trim());
7
+ }
8
+ function resolveByGuid(db, guid, kind) {
9
+ const candidates = [];
10
+ if (!kind || kind === "element") {
11
+ const rows = db.prepare("SELECT Object_ID, Object_Type, Name, Package_ID, ea_guid FROM t_object WHERE ea_guid = ? COLLATE NOCASE").all(guid);
12
+ for (const r of rows) {
13
+ candidates.push({
14
+ type: "element", id: r.Object_ID, name: r.Name,
15
+ match: "guid",
16
+ fullPackagePath: buildPackagePath(db, r.Package_ID),
17
+ eaGuid: r.ea_guid, objectType: r.Object_Type,
18
+ });
19
+ }
20
+ }
21
+ if (!kind || kind === "diagram") {
22
+ const rows = db.prepare("SELECT Diagram_ID, Name, Package_ID, ea_guid FROM t_diagram WHERE ea_guid = ? COLLATE NOCASE").all(guid);
23
+ for (const r of rows) {
24
+ candidates.push({
25
+ type: "diagram", id: r.Diagram_ID, name: r.Name,
26
+ match: "guid",
27
+ fullPackagePath: buildPackagePath(db, r.Package_ID),
28
+ eaGuid: r.ea_guid,
29
+ });
30
+ }
31
+ }
32
+ if (!kind || kind === "package") {
33
+ const rows = db.prepare("SELECT Package_ID, Name, Parent_ID, ea_guid FROM t_package WHERE ea_guid = ? COLLATE NOCASE").all(guid);
34
+ for (const r of rows) {
35
+ candidates.push({
36
+ type: "package", id: r.Package_ID, name: r.Name,
37
+ match: "guid",
38
+ fullPackagePath: buildPackagePath(db, r.Package_ID),
39
+ eaGuid: r.ea_guid,
40
+ });
41
+ }
42
+ }
43
+ return candidates;
44
+ }
45
+ function resolveByName(db, name, kind) {
46
+ const folded = foldText(name);
47
+ const prefix = `${folded}:`;
48
+ const exactCandidates = [];
49
+ const prefixCandidates = [];
50
+ const addCandidate = (candidateName, createCandidate) => {
51
+ const foldedCandidate = foldText(candidateName);
52
+ const match = foldedCandidate === folded
53
+ ? "exact"
54
+ : foldedCandidate.startsWith(prefix)
55
+ ? "prefix"
56
+ : undefined;
57
+ if (match) {
58
+ (match === "exact" ? exactCandidates : prefixCandidates).push({
59
+ ...createCandidate(),
60
+ match,
61
+ });
62
+ }
63
+ };
64
+ if (!kind || kind === "element") {
65
+ const rows = db.prepare("SELECT Object_ID, Object_Type, Name, Package_ID, ea_guid FROM t_object").all();
66
+ for (const r of rows) {
67
+ addCandidate(r.Name || "", () => ({
68
+ type: "element", id: r.Object_ID, name: r.Name,
69
+ fullPackagePath: buildPackagePath(db, r.Package_ID),
70
+ eaGuid: r.ea_guid, objectType: r.Object_Type,
71
+ }));
72
+ }
73
+ }
74
+ if (!kind || kind === "diagram") {
75
+ const rows = db.prepare("SELECT Diagram_ID, Name, Package_ID, ea_guid FROM t_diagram").all();
76
+ for (const r of rows) {
77
+ addCandidate(r.Name || "", () => ({
78
+ type: "diagram", id: r.Diagram_ID, name: r.Name,
79
+ fullPackagePath: buildPackagePath(db, r.Package_ID),
80
+ eaGuid: r.ea_guid,
81
+ }));
82
+ }
83
+ }
84
+ if (!kind || kind === "package") {
85
+ const rows = db.prepare("SELECT Package_ID, Name, Parent_ID, ea_guid FROM t_package").all();
86
+ for (const r of rows) {
87
+ addCandidate(r.Name || "", () => ({
88
+ type: "package", id: r.Package_ID, name: r.Name,
89
+ fullPackagePath: buildPackagePath(db, r.Package_ID),
90
+ eaGuid: r.ea_guid,
91
+ }));
92
+ }
93
+ }
94
+ return exactCandidates.length > 0 ? exactCandidates : prefixCandidates;
95
+ }
96
+ export function configureResolveTools(server, model) {
97
+ server.tool("ea_resolve", "Resolve an analyst reference (braced GUID or plain name) to model candidates: the input is echoed as `reference` and the hits are in `candidates`. A braced GUID is matched exactly. A plain name is matched against the full name first; only if nothing matches exactly is the reference retried as a name prefix, which resolves analyst codes like UC_ABC_2079 or OA_ABC_2280 to elements named 'CODE: description'. An exact hit is returned alone and is never diluted by prefix hits. Each candidate reports its `type` (element, diagram, package), `id`, `name`, `fullPackagePath`, `eaGuid`, and a `match` field that is always present with value \"guid\", \"exact\", or \"prefix\" — a \"prefix\" candidate is an inexact match and must not be treated as a confirmed identity. Use the optional `kind` filter to narrow results.", {
98
+ reference: z.string().describe("The reference to resolve: a braced GUID like {ABC-123} or a plain name"),
99
+ kind: z
100
+ .enum(["element", "diagram", "package"])
101
+ .optional()
102
+ .describe("Filter candidates to a specific kind"),
103
+ }, READ_ONLY, async ({ reference, kind }) => {
104
+ const db = await model.database();
105
+ try {
106
+ let candidates;
107
+ if (isBracedGuid(reference)) {
108
+ candidates = resolveByGuid(db, reference.trim(), kind);
109
+ }
110
+ else {
111
+ candidates = resolveByName(db, reference.trim(), kind);
112
+ }
113
+ return {
114
+ content: [{
115
+ type: "text",
116
+ text: JSON.stringify({
117
+ reference,
118
+ candidates,
119
+ totalMatched: candidates.length,
120
+ returned: candidates.length,
121
+ truncated: false,
122
+ _meta: { sourceTables: ["t_object", "t_diagram", "t_package"] },
123
+ }, null, 2),
124
+ }],
125
+ };
126
+ }
127
+ catch (error) {
128
+ const msg = error instanceof Error ? error.message : String(error);
129
+ return {
130
+ content: [{ type: "text", text: `Error resolving reference: ${msg}` }],
131
+ isError: true,
132
+ };
133
+ }
134
+ });
135
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ModelAccess } from "../model-session.js";
3
+ export declare function configureScenarioTools(server: McpServer, model: ModelAccess): void;
@@ -0,0 +1,106 @@
1
+ import { READ_ONLY } from "./annotations.js";
2
+ import { z } from "zod";
3
+ import { XMLParser } from "fast-xml-parser";
4
+ import { decodeEntities, compareNames } from "../text.js";
5
+ const xmlParser = new XMLParser({
6
+ ignoreAttributes: false,
7
+ attributeNamePrefix: "@_",
8
+ isArray: (name) => name === "step",
9
+ });
10
+ function parseScenarioXml(xml) {
11
+ if (!xml || xml.trim() === "")
12
+ return [];
13
+ try {
14
+ const parsed = xmlParser.parse(xml);
15
+ const steps = parsed?.path?.step;
16
+ if (!steps)
17
+ return [];
18
+ return (Array.isArray(steps) ? steps : [steps]).map((s) => ({
19
+ name: s["@_name"] || "",
20
+ level: parseInt(s["@_level"] || "0", 10),
21
+ guid: s["@_guid"] || "",
22
+ trigger: s["@_trigger"] || null,
23
+ uses: s["@_uses"] || null,
24
+ useslist: s["@_useslist"] || null,
25
+ result: s["@_result"] || null,
26
+ state: s["@_state"] || null,
27
+ link: s["@_link"] || null,
28
+ }));
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ // R9: Scenario type ordering — Basic Path first, then Alternate, then Exception
35
+ const SCENARIO_TYPE_ORDER = {
36
+ "Basic Path": 0,
37
+ "Alternate": 1,
38
+ "Exception": 2,
39
+ };
40
+ export function configureScenarioTools(server, model) {
41
+ server.tool("ea_get_scenarios", "Get use case scenario flows for an element. `scenarios` holds the parsed flows; each has `name`, `type`, `notes`, and `steps`, and each step carries `stepNumber` plus its attributes (`trigger`, `uses`, `result`, `state`, `link`). Steps are numbered within each scenario. Scenarios ordered by type: Basic Path first, then Alternate, then Exception.", {
42
+ elementId: z.coerce.number().describe("The Object_ID of the element (typically a UseCase) to get scenarios for"),
43
+ }, READ_ONLY, async ({ elementId }) => {
44
+ const db = await model.database();
45
+ try {
46
+ // Verify element exists
47
+ const elExists = db.prepare("SELECT Object_ID FROM t_object WHERE Object_ID = ?").get(elementId);
48
+ if (!elExists) {
49
+ return {
50
+ content: [{ type: "text", text: JSON.stringify({ error: "not_found", message: `Element with ID ${elementId} not found`, elementId }, null, 2) }],
51
+ isError: true,
52
+ };
53
+ }
54
+ const rows = db.prepare(`
55
+ SELECT Scenario, ScenarioType, XMLContent, Notes
56
+ FROM t_objectscenarios
57
+ WHERE Object_ID = ?
58
+ `).all(elementId);
59
+ if (rows.length === 0) {
60
+ return {
61
+ content: [{ type: "text", text: JSON.stringify({
62
+ scenarios: [],
63
+ totalMatched: 0,
64
+ returned: 0,
65
+ truncated: false,
66
+ _meta: { sourceTables: ["t_objectscenarios"] },
67
+ }, null, 2) }],
68
+ };
69
+ }
70
+ // R9: Sort by scenario type order, then by name within each type
71
+ rows.sort((a, b) => {
72
+ const aOrder = SCENARIO_TYPE_ORDER[a.ScenarioType] ?? 99;
73
+ const bOrder = SCENARIO_TYPE_ORDER[b.ScenarioType] ?? 99;
74
+ if (aOrder !== bOrder)
75
+ return aOrder - bOrder;
76
+ return compareNames(a.Scenario, b.Scenario);
77
+ });
78
+ const scenarios = rows.map((row) => {
79
+ const rawSteps = parseScenarioXml(row.XMLContent);
80
+ return {
81
+ name: row.Scenario,
82
+ type: row.ScenarioType,
83
+ notes: decodeEntities(row.Notes),
84
+ steps: rawSteps.map((s, i) => ({ stepNumber: i + 1, ...s })),
85
+ };
86
+ });
87
+ const response = {
88
+ scenarios,
89
+ totalMatched: scenarios.length,
90
+ returned: scenarios.length,
91
+ truncated: false,
92
+ _meta: { sourceTables: ["t_objectscenarios"] },
93
+ };
94
+ return {
95
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
96
+ };
97
+ }
98
+ catch (error) {
99
+ const msg = error instanceof Error ? error.message : String(error);
100
+ return {
101
+ content: [{ type: "text", text: `Error retrieving scenarios: ${msg}` }],
102
+ isError: true,
103
+ };
104
+ }
105
+ });
106
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type ModelAccess } from "../model-session.js";
3
+ export declare function configureSchemaTools(server: McpServer, model: ModelAccess): void;
@@ -0,0 +1,164 @@
1
+ import { describeSource } from "../model-session.js";
2
+ import { READ_ONLY } from "./annotations.js";
3
+ import { z } from "zod";
4
+ import { statSync } from "node:fs";
5
+ import { packageVersion } from "../version.js";
6
+ export function configureSchemaTools(server, model) {
7
+ server.tool("ea_get_schema", "List the model's database tables in `tables`, or pass a `tableName` to get that table's `columns` and `indexes` instead. That form echoes the `table` name and adds `rowidAlias` — the INTEGER PRIMARY KEY aliasing SQLite's rowid, so the fastest lookup path, or null when the table has none — with `rowidNote` saying which case applies. Use this to discover what data the model holds beyond what the typed ea_* tools return. See ea_get_model_info for the export's identity.", {
8
+ tableName: z
9
+ .string()
10
+ .optional()
11
+ .describe("Table name to inspect. Omit to list all tables with row counts."),
12
+ }, READ_ONLY, async ({ tableName }) => {
13
+ const db = await model.database();
14
+ try {
15
+ if (!tableName) {
16
+ const tables = db
17
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`)
18
+ .all();
19
+ const result = tables.map((t) => {
20
+ const row = db.prepare(`SELECT COUNT(*) as cnt FROM "${t.name}"`).get();
21
+ return { table: t.name, rowCount: row.cnt };
22
+ });
23
+ return {
24
+ content: [{ type: "text", text: JSON.stringify({
25
+ tables: result,
26
+ totalMatched: result.length,
27
+ returned: result.length,
28
+ truncated: false,
29
+ _meta: { sourceTables: ["sqlite_master"] },
30
+ }, null, 2) }],
31
+ };
32
+ }
33
+ // Verify table exists
34
+ const exists = db
35
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
36
+ .get(tableName);
37
+ if (!exists) {
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify({ error: "not_found", message: `Table '${tableName}' not found`, tableName }, null, 2) }],
40
+ isError: true,
41
+ };
42
+ }
43
+ // Columns — tableName is validated against sqlite_master above
44
+ const safeTableName = exists.name;
45
+ const columns = db.prepare(`PRAGMA table_info("${safeTableName}")`).all();
46
+ // Rowid alias detection: exactly one column with pk > 0 and declared type INTEGER (case-insensitive)
47
+ const pkColumns = columns.filter((c) => c.pk > 0);
48
+ const rowidAlias = pkColumns.length === 1 && pkColumns[0].type.toUpperCase() === "INTEGER"
49
+ ? pkColumns[0].name
50
+ : null;
51
+ // Indexes
52
+ const indexList = db.prepare(`PRAGMA index_list("${safeTableName}")`).all();
53
+ const indexes = indexList.map((idx) => {
54
+ const indexCols = db.prepare(`PRAGMA index_info("${idx.name}")`).all();
55
+ return {
56
+ name: idx.name,
57
+ unique: idx.unique === 1,
58
+ columns: indexCols.map((c) => c.name),
59
+ };
60
+ });
61
+ const result = {
62
+ table: tableName,
63
+ columns: columns.map((c) => ({
64
+ name: c.name,
65
+ type: c.type,
66
+ notNull: c.notnull === 1,
67
+ primaryKey: c.pk > 0,
68
+ defaultValue: c.dflt_value,
69
+ })),
70
+ indexes,
71
+ _meta: {
72
+ sourceTables: ["sqlite_master"],
73
+ columns: { totalMatched: columns.length, returned: columns.length, truncated: false },
74
+ indexes: { totalMatched: indexes.length, returned: indexes.length, truncated: false },
75
+ },
76
+ };
77
+ if (rowidAlias) {
78
+ result.rowidAlias = rowidAlias;
79
+ result.rowidNote =
80
+ "This column is an INTEGER PRIMARY KEY that aliases SQLite's internal rowid. Lookups by this column are the fastest access path.";
81
+ }
82
+ else {
83
+ result.rowidAlias = null;
84
+ result.rowidNote = "This table has no single-column INTEGER PRIMARY KEY rowid alias.";
85
+ }
86
+ return {
87
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
88
+ };
89
+ }
90
+ catch (error) {
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: `Error reading schema: ${error instanceof Error ? error.message : String(error)}`,
96
+ },
97
+ ],
98
+ isError: true,
99
+ };
100
+ }
101
+ });
102
+ server.tool("ea_get_model_info", "Report which .qea export file the server has open: `fileName` is the citable identity, alongside `fileSizeBytes`, `lastModified`, and the `serverVersion` that produced the answer. The full local path is also returned as `resolvedPath`, with `resolvedPathNote` explaining why it is environment detail rather than something to cite. `configuration` says where that path came from — `source` in words, `sourceId` as one of argument/environment/dotenv/remembered/prompt, and the `configured` value behind it — plus any `skipped` settings, each with the `reason` it could not be opened, and `shadowed` ones a higher-priority source outranked; `configurationNote` says how much of that is safe to repeat.", {}, READ_ONLY, async () => {
103
+ const db = await model.database();
104
+ try {
105
+ const location = db.location();
106
+ if (!location) {
107
+ return {
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: "Model info unavailable: database has no file location (in-memory database).",
112
+ },
113
+ ],
114
+ isError: true,
115
+ };
116
+ }
117
+ const stat = statSync(location);
118
+ const fileName = location.replace(/\\/g, "/").split("/").pop() ?? location;
119
+ const origin = model.origin();
120
+ const result = {
121
+ fileName,
122
+ fileSizeBytes: stat.size,
123
+ lastModified: stat.mtime.toISOString(),
124
+ serverVersion: packageVersion,
125
+ resolvedPath: location,
126
+ resolvedPathNote: "The resolved path is local detail — it may contain user-specific directories. Use fileName, size, and lastModified as the citable identity.",
127
+ ...(origin && {
128
+ configuration: {
129
+ source: describeSource(origin.source),
130
+ sourceId: origin.source,
131
+ configured: origin.configured,
132
+ skipped: origin.ignored.map((entry) => ({
133
+ source: describeSource(entry.source),
134
+ sourceId: entry.source,
135
+ configured: entry.configured,
136
+ reason: entry.reason,
137
+ })),
138
+ shadowed: origin.shadowed.map((entry) => ({
139
+ source: describeSource(entry.source),
140
+ sourceId: entry.source,
141
+ configured: entry.configured,
142
+ })),
143
+ },
144
+ configurationNote: "Everything under configuration is local environment detail: configured values are filesystem paths and each skipped reason quotes one in full. Repeat them only when explaining a configuration problem to the user who owns the machine, never as model identity.",
145
+ }),
146
+ _meta: { sourceTables: [] },
147
+ };
148
+ return {
149
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
150
+ };
151
+ }
152
+ catch (error) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: `Error reading model info: ${error instanceof Error ? error.message : String(error)}`,
158
+ },
159
+ ],
160
+ isError: true,
161
+ };
162
+ }
163
+ });
164
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ModelAccess } from "../model-session.js";
3
+ export declare function configureSearchTools(server: McpServer, model: ModelAccess): void;