KISC arch init
This commit is contained in:
19
ikt-arh-kultura-valodu-tehnologijas/mcp/Dockerfile
Normal file
19
ikt-arh-kultura-valodu-tehnologijas/mcp/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy MCP server code
|
||||
COPY mcp/package.json mcp/tsconfig.json /app/mcp/
|
||||
COPY mcp/src /app/mcp/src
|
||||
|
||||
# Copy architecture data (SSOT)
|
||||
COPY domains /app/domains
|
||||
|
||||
WORKDIR /app/mcp
|
||||
RUN npm ci || npm install
|
||||
RUN npm run build
|
||||
|
||||
ENV ARCH_ROOT=/app/domains
|
||||
EXPOSE 8787
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
2119
ikt-arh-kultura-valodu-tehnologijas/mcp/package-lock.json
generated
Normal file
2119
ikt-arh-kultura-valodu-tehnologijas/mcp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
ikt-arh-kultura-valodu-tehnologijas/mcp/package.json
Normal file
27
ikt-arh-kultura-valodu-tehnologijas/mcp/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "kisc-arch-mcp",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"gen:doc": "ts-node src/generateDoc.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^4.21.2",
|
||||
"fast-glob": "^3.3.2",
|
||||
"jose": "^5.10.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.17.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
285
ikt-arh-kultura-valodu-tehnologijas/mcp/src/generateDoc.ts
Normal file
285
ikt-arh-kultura-valodu-tehnologijas/mcp/src/generateDoc.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
type AnyObj = Record<string, any>;
|
||||
|
||||
const REPO_ROOT = process.env.REPO_ROOT || path.resolve(__dirname, "../../");
|
||||
const DOMAIN_DIR = process.env.DOMAIN_DIR || "domains/kultura-valoda";
|
||||
const OUT_FILE = process.env.OUT_FILE || "KISC-merkarhitektura-apraksts.md";
|
||||
|
||||
async function readText(rel: string): Promise<string> {
|
||||
const abs = path.join(REPO_ROOT, rel);
|
||||
return fs.readFile(abs, "utf-8");
|
||||
}
|
||||
|
||||
async function readYaml(rel: string): Promise<AnyObj> {
|
||||
const abs = path.join(REPO_ROOT, rel);
|
||||
const raw = await fs.readFile(abs, "utf-8");
|
||||
const obj = yaml.load(raw);
|
||||
if (!obj || typeof obj !== "object") return {};
|
||||
return obj as AnyObj;
|
||||
}
|
||||
|
||||
function mdEscape(s: any): string {
|
||||
const t = (s ?? "").toString();
|
||||
return t.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
||||
}
|
||||
|
||||
function mdTable(headers: string[], rows: string[][]): string {
|
||||
const h = `| ${headers.join(" | ")} |`;
|
||||
const sep = `| ${headers.map(() => "---").join(" | ")} |`;
|
||||
const body = rows.map((r) => `| ${r.join(" | ")} |`).join("\n");
|
||||
return [h, sep, body].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
function isRegisterRefLine(line: string): string | null {
|
||||
// Matches: `registers/..../file.yaml` or `registers/..../file.yml`
|
||||
const m = line.match(/`(registers\/[^`]+\.(yaml|yml))`/i);
|
||||
return m?.[1] ?? null;
|
||||
}
|
||||
|
||||
function isDiagramRefLine(line: string): string | null {
|
||||
const m = line.match(/`(diagrams\/[^`]+\.(mmd|png|svg|pdf))`/i);
|
||||
return m?.[1] ?? null;
|
||||
}
|
||||
|
||||
function renderAbbreviations(obj: AnyObj): string {
|
||||
const items = Array.isArray(obj.items) ? obj.items : [];
|
||||
const rows = items.map((it: any) => [mdEscape(it.abbr), mdEscape(it.meaning)]);
|
||||
return mdTable(["Saīsinājums", "Nozīme"], rows);
|
||||
}
|
||||
|
||||
function renderTerms(obj: AnyObj): string {
|
||||
const items = Array.isArray(obj.items) ? obj.items : [];
|
||||
// Accept both {term, meaning} and {name, definition}
|
||||
const rows = items.map((it: any) => [
|
||||
mdEscape(it.term ?? it.name ?? it.id ?? ""),
|
||||
mdEscape(it.meaning ?? it.definition ?? it.description ?? ""),
|
||||
]);
|
||||
return mdTable(["Termins", "Skaidrojums"], rows);
|
||||
}
|
||||
|
||||
function renderRelatedDocs(obj: AnyObj): string {
|
||||
const items = Array.isArray(obj.items) ? obj.items : [];
|
||||
const rows = items.map((it: any) => [
|
||||
mdEscape(it.id),
|
||||
mdEscape(it.title),
|
||||
mdEscape(it.date ?? ""),
|
||||
mdEscape(it.type ?? ""),
|
||||
mdEscape(it.relevance ?? ""),
|
||||
]);
|
||||
return mdTable(["ID", "Nosaukums", "Datums", "Veids", "Saistība / nozīme"], rows);
|
||||
}
|
||||
|
||||
function renderOrganizations(obj: AnyObj): string {
|
||||
const items = Array.isArray(obj.items) ? obj.items : [];
|
||||
const rows = items.map((it: any) => [
|
||||
mdEscape(it.id),
|
||||
mdEscape(it.title),
|
||||
mdEscape(it.role ?? ""),
|
||||
mdEscape(Array.isArray(it.responsibilities) ? it.responsibilities.join("; ") : ""),
|
||||
mdEscape(it.status ?? ""),
|
||||
]);
|
||||
return mdTable(["ID", "Organizācija", "Loma", "Atbildība (kopsavilkums)", "Statuss"], rows);
|
||||
}
|
||||
|
||||
function renderGenericItemsTable(obj: AnyObj, preferredCols: string[] | null = null): string {
|
||||
const items = Array.isArray(obj.items) ? obj.items : [];
|
||||
if (!items.length) return "_(tukšs reģistrs)_";
|
||||
|
||||
const allKeys = new Set<string>();
|
||||
for (const it of items) {
|
||||
if (it && typeof it === "object") Object.keys(it).forEach((k) => allKeys.add(k));
|
||||
}
|
||||
|
||||
const cols = preferredCols
|
||||
? preferredCols.filter((c) => allKeys.has(c)).concat([...allKeys].filter((k) => !preferredCols.includes(k)))
|
||||
: [...allKeys];
|
||||
|
||||
const headers = cols.map((c) => c);
|
||||
const rows = items.map((it: any) =>
|
||||
cols.map((c) => {
|
||||
const v = it?.[c];
|
||||
if (Array.isArray(v)) return mdEscape(v.join(", "));
|
||||
if (v && typeof v === "object") return mdEscape(JSON.stringify(v));
|
||||
return mdEscape(v ?? "");
|
||||
})
|
||||
);
|
||||
|
||||
return mdTable(headers, rows);
|
||||
}
|
||||
|
||||
function renderLegalActs(obj: AnyObj): string {
|
||||
const groups = Array.isArray(obj.groups) ? obj.groups : [];
|
||||
if (!groups.length) return "_(tukšs reģistrs)_";
|
||||
|
||||
const out: string[] = [];
|
||||
for (const g of groups) {
|
||||
out.push(`### ${mdEscape(g.title ?? g.id ?? "")}`.trim());
|
||||
const items = Array.isArray(g.items) ? g.items : [];
|
||||
if (!items.length) {
|
||||
out.push("_(nav ierakstu)_");
|
||||
out.push("");
|
||||
continue;
|
||||
}
|
||||
for (const it of items) {
|
||||
const ref = mdEscape(it.ref ?? "");
|
||||
const notes = mdEscape(it.notes ?? "");
|
||||
out.push(`- **${ref}** — ${notes}`.trim());
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
return out.join("\n").trim();
|
||||
}
|
||||
|
||||
function renderRoadmap(obj: AnyObj): string {
|
||||
const items = Array.isArray(obj.items) ? obj.items : [];
|
||||
if (!items.length) return "_(tukšs reģistrs)_";
|
||||
|
||||
const rows = items.map((it: any) => [
|
||||
mdEscape(it.id),
|
||||
mdEscape(it.title),
|
||||
mdEscape(it.timeframe ?? ""),
|
||||
mdEscape(Array.isArray(it.owner_orgs) ? it.owner_orgs.join(", ") : ""),
|
||||
mdEscape(Array.isArray(it.depends_on) ? it.depends_on.join(", ") : ""),
|
||||
mdEscape(Array.isArray(it.outputs) ? it.outputs.join("; ") : ""),
|
||||
]);
|
||||
|
||||
return mdTable(["ID", "Pasākums", "Laika posms", "Atbildīgie", "Atkarības", "Rezultāti"], rows);
|
||||
}
|
||||
|
||||
function renderRisks(obj: AnyObj): string {
|
||||
const items = Array.isArray(obj.items) ? obj.items : [];
|
||||
if (!items.length) return "_(tukšs reģistrs)_";
|
||||
|
||||
const rows = items.map((it: any) => [
|
||||
mdEscape(it.id),
|
||||
mdEscape(it.risk),
|
||||
mdEscape(it.likelihood ?? ""),
|
||||
mdEscape(it.impact ?? ""),
|
||||
mdEscape(Array.isArray(it.mitigation) ? it.mitigation.join("; ") : it.mitigation ?? ""),
|
||||
mdEscape(Array.isArray(it.owner_orgs) ? it.owner_orgs.join(", ") : ""),
|
||||
]);
|
||||
|
||||
return mdTable(["ID", "Risks", "Varbūtība", "Ietekme", "Mazināšana", "Atbildīgie"], rows);
|
||||
}
|
||||
|
||||
function renderInteractions(obj: AnyObj): string {
|
||||
const items = Array.isArray(obj.items) ? obj.items : [];
|
||||
if (!items.length) return "_(tukšs reģistrs)_";
|
||||
|
||||
const out: string[] = [];
|
||||
for (const it of items) {
|
||||
out.push(`### ${mdEscape(it.title ?? it.id ?? "")}`.trim());
|
||||
out.push(`- **ID:** ${mdEscape(it.id)}`);
|
||||
out.push(`- **Tips:** ${mdEscape(it.interaction_type ?? "")}`);
|
||||
out.push(`- **Pretējā puse:** ${mdEscape(it.counterpart ?? "")}`);
|
||||
if (Array.isArray(it.components) && it.components.length) {
|
||||
out.push(`- **Komponentes:** ${mdEscape(it.components.join(", "))}`);
|
||||
}
|
||||
if (it.why_it_matters) {
|
||||
out.push(`- **Kāpēc svarīgi:** ${mdEscape(it.why_it_matters)}`);
|
||||
}
|
||||
if (Array.isArray(it.requirements) && it.requirements.length) {
|
||||
out.push(`- **Prasības:** ${mdEscape(it.requirements.join("; "))}`);
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
return out.join("\n").trim();
|
||||
}
|
||||
|
||||
function renderRegisterByPath(relRegisterPathFromDomain: string, domainRoot: string): Promise<string> {
|
||||
const fullRel = path.posix.join(domainRoot, relRegisterPathFromDomain);
|
||||
return (async () => {
|
||||
const obj = await readYaml(fullRel);
|
||||
|
||||
// Render by filename conventions
|
||||
const file = relRegisterPathFromDomain.replace(/\\/g, "/");
|
||||
|
||||
if (file.endsWith("abbreviations.yaml") || file.endsWith("abbreviations.yml")) return renderAbbreviations(obj);
|
||||
if (file.endsWith("terms.yaml") || file.endsWith("terms.yml")) return renderTerms(obj);
|
||||
if (file.endsWith("related-documents.yaml") || file.endsWith("related-documents.yml")) return renderRelatedDocs(obj);
|
||||
if (file.endsWith("organizations.yaml") || file.endsWith("organizations.yml")) return renderOrganizations(obj);
|
||||
if (file.endsWith("legal-acts.yaml") || file.endsWith("legal-acts.yml")) return renderLegalActs(obj);
|
||||
if (file.endsWith("roadmap.yaml") || file.endsWith("roadmap.yml")) return renderRoadmap(obj);
|
||||
if (file.endsWith("risks.yaml") || file.endsWith("risks.yml")) return renderRisks(obj);
|
||||
if (file.endsWith("interactions.yaml") || file.endsWith("interactions.yml")) return renderInteractions(obj);
|
||||
|
||||
// Known item-heavy registers: functions/services/info-resources/systems: use a nicer preferred order
|
||||
const preferred = (() => {
|
||||
if (file.includes("/04-functions/")) return ["id", "subdomain", "name", "change_status", "change_description"];
|
||||
if (file.includes("/05-services/")) return ["id", "subdomain", "name", "change_status", "change_description"];
|
||||
if (file.includes("/06-information-resources/"))
|
||||
return ["id", "subdomain", "name", "type", "owner", "change_status", "change_description"];
|
||||
if (file.includes("/07-systems/")) return ["id", "subdomain", "name", "owner", "change_status", "change_description"];
|
||||
return null;
|
||||
})();
|
||||
|
||||
return renderGenericItemsTable(obj, preferred);
|
||||
})();
|
||||
}
|
||||
|
||||
async function build() {
|
||||
const domainRoot = DOMAIN_DIR.replace(/\\/g, "/");
|
||||
const manifestRel = path.posix.join(domainRoot, "manifest.yaml");
|
||||
const manifest = await readYaml(manifestRel);
|
||||
|
||||
if (!Array.isArray(manifest.views)) {
|
||||
throw new Error(`manifest.yaml missing 'views' array: ${manifestRel}`);
|
||||
}
|
||||
|
||||
const pieces: string[] = [];
|
||||
pieces.push(`# ${manifest.title ?? "Dokuments"}`);
|
||||
if (manifest.version) pieces.push(`\n_Versija: ${manifest.version}_\n`);
|
||||
|
||||
for (const v of manifest.views) {
|
||||
const viewFile = v?.file;
|
||||
if (!viewFile) continue;
|
||||
|
||||
const viewRel = path.posix.join(domainRoot, viewFile);
|
||||
let md = await readText(viewRel);
|
||||
|
||||
const outLines: string[] = [];
|
||||
const lines = md.split(/\r?\n/);
|
||||
|
||||
for (const line of lines) {
|
||||
const regRef = isRegisterRefLine(line);
|
||||
const diagramRef = isDiagramRefLine(line);
|
||||
|
||||
if (regRef) {
|
||||
// keep the original line, then render the register below it
|
||||
outLines.push(line);
|
||||
const rendered = await renderRegisterByPath(regRef, domainRoot);
|
||||
outLines.push("");
|
||||
outLines.push(rendered);
|
||||
outLines.push("");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (diagramRef) {
|
||||
// keep diagram placeholder as-is (per your rule)
|
||||
outLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
outLines.push(line);
|
||||
}
|
||||
|
||||
pieces.push(outLines.join("\n").trimEnd());
|
||||
pieces.push(""); // spacing between views
|
||||
}
|
||||
|
||||
const finalMd = pieces.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
||||
const outAbs = path.join(REPO_ROOT, OUT_FILE);
|
||||
await fs.writeFile(outAbs, finalMd, "utf-8");
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`OK: generated ${OUT_FILE}`);
|
||||
}
|
||||
|
||||
build().catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
1730
ikt-arh-kultura-valodu-tehnologijas/mcp/src/index.ts
Normal file
1730
ikt-arh-kultura-valodu-tehnologijas/mcp/src/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
15
ikt-arh-kultura-valodu-tehnologijas/mcp/src/repo/index.ts
Normal file
15
ikt-arh-kultura-valodu-tehnologijas/mcp/src/repo/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { loadRepo } from "./load.js";
|
||||
import { RepoIndex } from "./types.js";
|
||||
|
||||
let cached: RepoIndex | null = null;
|
||||
|
||||
export function getIndex(archRoot: string): RepoIndex {
|
||||
if (!cached) cached = loadRepo(archRoot);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Optional: reload endpoint support (not exposed yet)
|
||||
export function reload(archRoot: string): RepoIndex {
|
||||
cached = loadRepo(archRoot);
|
||||
return cached;
|
||||
}
|
||||
195
ikt-arh-kultura-valodu-tehnologijas/mcp/src/repo/load.ts
Normal file
195
ikt-arh-kultura-valodu-tehnologijas/mcp/src/repo/load.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import fg from "fast-glob";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import yaml from "js-yaml";
|
||||
import { Entity, Edge, RepoIndex } from "./types.js";
|
||||
|
||||
function toKindFromId(id: string): Entity["_kind"] {
|
||||
if (id.startsWith("goal.")) return "goal";
|
||||
if (id.startsWith("func.")) return "function";
|
||||
if (
|
||||
id.startsWith("svc.kultura.") ||
|
||||
id.startsWith("svc.valoda.") ||
|
||||
id.startsWith("svc.")
|
||||
)
|
||||
return "service";
|
||||
if (
|
||||
id.startsWith("ir.kultura.") ||
|
||||
id.startsWith("ir.valoda.") ||
|
||||
id.startsWith("ir.")
|
||||
)
|
||||
return "information_resource";
|
||||
if (
|
||||
id.startsWith("sys.kultura.") ||
|
||||
id.startsWith("sys.valoda.") ||
|
||||
id.startsWith("sys.")
|
||||
)
|
||||
return "system";
|
||||
if (id.startsWith("principle.")) return "principle";
|
||||
if (id.startsWith("org.")) return "org";
|
||||
if (id.startsWith("domain.")) return "domain";
|
||||
if (id.startsWith("subdomain.")) return "subdomain";
|
||||
if (id.startsWith("rm.")) return "roadmap";
|
||||
if (id.startsWith("risk.")) return "risk";
|
||||
if (id.startsWith("int.")) return "interaction";
|
||||
if (id.startsWith("doc.")) return "document";
|
||||
if (id.startsWith("legal.")) return "legal";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function safeString(x: unknown): string {
|
||||
if (x === null || x === undefined) return "";
|
||||
if (typeof x === "string") return x;
|
||||
if (typeof x === "number" || typeof x === "boolean") return String(x);
|
||||
try {
|
||||
return JSON.stringify(x);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function indexEntity(idx: RepoIndex, e: Entity) {
|
||||
idx.entitiesById.set(e.id, e);
|
||||
const hay = [
|
||||
e.id,
|
||||
e._kind,
|
||||
e._domain ?? "",
|
||||
e._subdomain ?? "",
|
||||
safeString(e.title),
|
||||
safeString(e.name),
|
||||
safeString(e.description),
|
||||
safeString(e.change_description),
|
||||
safeString(e.status),
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
|
||||
idx.textIndex.push({
|
||||
id: e.id,
|
||||
haystack: hay,
|
||||
kind: e._kind,
|
||||
domain: e._domain,
|
||||
subdomain: e._subdomain,
|
||||
});
|
||||
}
|
||||
|
||||
function readYaml(filePath: string): any {
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
return yaml.load(raw);
|
||||
}
|
||||
|
||||
export function loadRepo(archRoot: string): RepoIndex {
|
||||
const idx: RepoIndex = {
|
||||
entitiesById: new Map(),
|
||||
edges: [],
|
||||
textIndex: [],
|
||||
resources: [],
|
||||
};
|
||||
|
||||
// Register resource URIs for views and manifest and raw registers.
|
||||
const domainManifests = fg.sync(["**/manifest.yaml"], {
|
||||
cwd: archRoot,
|
||||
dot: false,
|
||||
absolute: true,
|
||||
});
|
||||
for (const mf of domainManifests) {
|
||||
const mfObj: any = readYaml(mf);
|
||||
const domainFolder = path.dirname(mf);
|
||||
const domainSlug = path.basename(domainFolder);
|
||||
|
||||
idx.resources.push({
|
||||
uri: `manifest://${domainSlug}`,
|
||||
title: `Manifest: ${domainSlug}`,
|
||||
mimeType: "application/yaml",
|
||||
});
|
||||
|
||||
// Views resources
|
||||
const views = (mfObj?.views ?? []) as Array<{
|
||||
file: string;
|
||||
title?: string;
|
||||
id?: string | number;
|
||||
}>;
|
||||
for (const v of views) {
|
||||
const name = path.basename(v.file, path.extname(v.file));
|
||||
idx.resources.push({
|
||||
uri: `views://${domainSlug}/${name}`,
|
||||
title: `View: ${domainSlug} / ${v.title ?? name}`,
|
||||
mimeType: "text/markdown",
|
||||
});
|
||||
}
|
||||
|
||||
// Register raw YAML registers as resources
|
||||
const regFiles = fg.sync(["registers/**/*.yaml"], {
|
||||
cwd: domainFolder,
|
||||
absolute: true,
|
||||
});
|
||||
for (const rf of regFiles) {
|
||||
const rel = path.relative(domainFolder, rf).replaceAll("\\", "/");
|
||||
idx.resources.push({
|
||||
uri: `register://${domainSlug}/${rel}`,
|
||||
title: `Register: ${domainSlug}/${rel}`,
|
||||
mimeType: "application/yaml",
|
||||
});
|
||||
}
|
||||
|
||||
// Load domain entity (if present)
|
||||
const domainYaml = path.join(domainFolder, "registers/00-meta/domain.yaml");
|
||||
if (fs.existsSync(domainYaml)) {
|
||||
const d = readYaml(domainYaml);
|
||||
const ent: Entity = {
|
||||
...(d ?? {}),
|
||||
id: d?.id ?? `domain.${domainSlug}`,
|
||||
_kind: "domain",
|
||||
_domain: `domain.${domainSlug}`,
|
||||
_source_path: path.relative(archRoot, domainYaml).replaceAll("\\", "/"),
|
||||
};
|
||||
indexEntity(idx, ent);
|
||||
}
|
||||
|
||||
// Load YAML registers with "items"
|
||||
const regList = fg.sync(["registers/**/*.yaml"], {
|
||||
cwd: domainFolder,
|
||||
absolute: true,
|
||||
});
|
||||
for (const file of regList) {
|
||||
const obj = readYaml(file);
|
||||
|
||||
// edges file
|
||||
if (file.endsWith("registers/99-relations/edges.yaml") && obj?.edges) {
|
||||
const edges = obj.edges as Edge[];
|
||||
idx.edges.push(...edges);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Single-entity YAML (has id but no items)
|
||||
if (obj?.id && !obj?.items) {
|
||||
const ent: Entity = {
|
||||
...(obj ?? {}),
|
||||
_kind: toKindFromId(obj.id),
|
||||
_domain: `domain.${domainSlug}`,
|
||||
_subdomain: obj?.subdomain ?? undefined,
|
||||
_source_path: path.relative(archRoot, file).replaceAll("\\", "/"),
|
||||
};
|
||||
indexEntity(idx, ent);
|
||||
continue;
|
||||
}
|
||||
|
||||
// List YAML (items)
|
||||
if (Array.isArray(obj?.items)) {
|
||||
for (const it of obj.items) {
|
||||
if (!it?.id) continue;
|
||||
const ent: Entity = {
|
||||
...(it ?? {}),
|
||||
_kind: toKindFromId(it.id),
|
||||
_domain: `domain.${domainSlug}`,
|
||||
_subdomain: it?.subdomain ?? undefined,
|
||||
_source_path: path.relative(archRoot, file).replaceAll("\\", "/"),
|
||||
};
|
||||
indexEntity(idx, ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return idx;
|
||||
}
|
||||
44
ikt-arh-kultura-valodu-tehnologijas/mcp/src/repo/types.ts
Normal file
44
ikt-arh-kultura-valodu-tehnologijas/mcp/src/repo/types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export type DomainId = string;
|
||||
|
||||
export type Edge = {
|
||||
from: string;
|
||||
type: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
export type Entity = {
|
||||
id: string;
|
||||
_kind:
|
||||
| "goal"
|
||||
| "function"
|
||||
| "service"
|
||||
| "information_resource"
|
||||
| "system"
|
||||
| "principle"
|
||||
| "org"
|
||||
| "domain"
|
||||
| "subdomain"
|
||||
| "roadmap"
|
||||
| "risk"
|
||||
| "interaction"
|
||||
| "document"
|
||||
| "legal"
|
||||
| "unknown";
|
||||
_domain?: DomainId;
|
||||
_subdomain?: string;
|
||||
_source_path?: string;
|
||||
[k: string]: any;
|
||||
};
|
||||
|
||||
export type RepoIndex = {
|
||||
entitiesById: Map<string, Entity>;
|
||||
edges: Edge[];
|
||||
textIndex: Array<{
|
||||
id: string;
|
||||
haystack: string;
|
||||
kind: string;
|
||||
domain?: string;
|
||||
subdomain?: string;
|
||||
}>;
|
||||
resources: Array<{ uri: string; title: string; mimeType: string }>;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import fg from "fast-glob";
|
||||
|
||||
function readFileSafe(filePath: string): string {
|
||||
return fs.readFileSync(filePath, "utf-8");
|
||||
}
|
||||
|
||||
export function getResource(archRoot: string, uri: string) {
|
||||
// manifest://<domain>
|
||||
if (uri.startsWith("manifest://")) {
|
||||
const domain = uri.replace("manifest://", "");
|
||||
const mf = path.join(archRoot, domain, "manifest.yaml");
|
||||
if (!fs.existsSync(mf)) return { ok: false, error: `Not found: ${uri}` };
|
||||
return { ok: true, uri, mimeType: "application/yaml", content: readFileSafe(mf) };
|
||||
}
|
||||
|
||||
// views://<domain>/<viewfile-without-ext>
|
||||
if (uri.startsWith("views://")) {
|
||||
const rest = uri.replace("views://", "");
|
||||
const [domain, viewName] = rest.split("/", 2);
|
||||
const domainDir = path.join(archRoot, domain);
|
||||
const matches = fg.sync([`views/${viewName}.md`], { cwd: domainDir, absolute: true });
|
||||
if (!matches.length) return { ok: false, error: `Not found: ${uri}` };
|
||||
return {
|
||||
ok: true,
|
||||
uri,
|
||||
mimeType: "text/markdown",
|
||||
content: readFileSafe(matches[0]!),
|
||||
};
|
||||
}
|
||||
|
||||
// register://<domain>/<relative-path>
|
||||
if (uri.startsWith("register://")) {
|
||||
const rest = uri.replace("register://", "");
|
||||
const [domain, rel] = rest.split("/", 2);
|
||||
const filePath = path.join(archRoot, domain, rel);
|
||||
if (!fs.existsSync(filePath)) return { ok: false, error: `Not found: ${uri}` };
|
||||
return { ok: true, uri, mimeType: "application/yaml", content: readFileSafe(filePath) };
|
||||
}
|
||||
|
||||
return { ok: false, error: `Unsupported uri: ${uri}` };
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { RepoIndex } from "../repo/types.js";
|
||||
|
||||
export function listResources(idx: RepoIndex) {
|
||||
return {
|
||||
ok: true,
|
||||
resources: idx.resources,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { RepoIndex } from "../repo/types.js";
|
||||
|
||||
export function getEntityTool(idx: RepoIndex, args: any) {
|
||||
const id = String(args?.id ?? "").trim();
|
||||
if (!id) return { ok: false, error: "Missing args.id" };
|
||||
|
||||
const ent = idx.entitiesById.get(id);
|
||||
if (!ent) return { ok: false, error: `Entity not found: ${id}` };
|
||||
|
||||
return { ok: true, entity: ent };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { RepoIndex } from "../repo/types.js";
|
||||
|
||||
export function listRelationsTool(idx: RepoIndex, args: any) {
|
||||
const id = String(args?.id ?? "").trim();
|
||||
if (!id) return { ok: false, error: "Missing args.id" };
|
||||
|
||||
const direction = String(args?.direction ?? "both"); // in|out|both
|
||||
|
||||
const edges = idx.edges.filter((e) => {
|
||||
if (direction === "out") return e.from === id;
|
||||
if (direction === "in") return e.to === id;
|
||||
return e.from === id || e.to === id;
|
||||
});
|
||||
|
||||
return { ok: true, edges };
|
||||
}
|
||||
29
ikt-arh-kultura-valodu-tehnologijas/mcp/src/tools/search.ts
Normal file
29
ikt-arh-kultura-valodu-tehnologijas/mcp/src/tools/search.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { RepoIndex } from "../repo/types.js";
|
||||
|
||||
export function searchTool(idx: RepoIndex, args: any) {
|
||||
const q = String(args?.query ?? "").toLowerCase().trim();
|
||||
if (!q) return { ok: false, error: "Missing args.query" };
|
||||
|
||||
const kind = args?.kind ? String(args.kind) : null;
|
||||
const subdomain = args?.subdomain ? String(args.subdomain) : null;
|
||||
|
||||
const hits: Array<{ id: string; score: number; kind: string; subdomain?: string }> = [];
|
||||
|
||||
for (const row of idx.textIndex) {
|
||||
if (kind && row.kind !== kind) continue;
|
||||
if (subdomain && row.subdomain !== subdomain) continue;
|
||||
|
||||
// Very simple scoring: count substring occurrences
|
||||
const hay = row.haystack;
|
||||
let score = 0;
|
||||
let pos = hay.indexOf(q);
|
||||
while (pos !== -1) {
|
||||
score++;
|
||||
pos = hay.indexOf(q, pos + q.length);
|
||||
}
|
||||
if (score > 0) hits.push({ id: row.id, score, kind: row.kind, subdomain: row.subdomain });
|
||||
}
|
||||
|
||||
hits.sort((a, b) => b.score - a.score);
|
||||
return { ok: true, results: hits.slice(0, 50) };
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { RepoIndex } from "../repo/types.js";
|
||||
|
||||
export function subgraphTool(idx: RepoIndex, args: any) {
|
||||
const seeds = Array.isArray(args?.seed_ids)
|
||||
? args.seed_ids.map((x: any) => String(x))
|
||||
: [];
|
||||
const depth = Number.isFinite(args?.depth) ? Number(args.depth) : 1;
|
||||
|
||||
if (!seeds.length) return { ok: false, error: "Missing args.seed_ids[]" };
|
||||
if (depth < 0 || depth > 5) return { ok: false, error: "depth must be 0..5" };
|
||||
|
||||
const nodes = new Set<string>(seeds);
|
||||
const edgesOut: any[] = [];
|
||||
|
||||
let frontier = new Set<string>(seeds);
|
||||
|
||||
for (let d = 0; d < depth; d++) {
|
||||
const next = new Set<string>();
|
||||
|
||||
for (const e of idx.edges) {
|
||||
if (frontier.has(e.from)) {
|
||||
edgesOut.push(e);
|
||||
if (!nodes.has(e.to)) next.add(e.to);
|
||||
nodes.add(e.to);
|
||||
}
|
||||
if (frontier.has(e.to)) {
|
||||
edgesOut.push(e);
|
||||
if (!nodes.has(e.from)) next.add(e.from);
|
||||
nodes.add(e.from);
|
||||
}
|
||||
}
|
||||
|
||||
frontier = next;
|
||||
if (!frontier.size) break;
|
||||
}
|
||||
|
||||
const nodeObjs = Array.from(nodes).map(
|
||||
(id) => idx.entitiesById.get(id) ?? { id, _kind: "unknown" }
|
||||
);
|
||||
|
||||
return { ok: true, nodes: nodeObjs, edges: edgesOut };
|
||||
}
|
||||
15
ikt-arh-kultura-valodu-tehnologijas/mcp/tsconfig.json
Normal file
15
ikt-arh-kultura-valodu-tehnologijas/mcp/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user