Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions extensions/mcp-server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ export const mcpServerConfigSchema = {
}

const host = typeof cfg.host === "string" ? cfg.host : DEFAULT_HOST;
if (!/^[a-zA-Z0-9._-]+$/.test(host)) {
throw new Error(
`Invalid host: "${host}". Must contain only alphanumeric, dots, hyphens, or underscores.`,
);
}

const auth = parseAuthConfig(cfg.auth);
const capabilities = parseCapabilities(cfg.capabilities);
Expand Down
112 changes: 67 additions & 45 deletions extensions/mcp-server/cortex-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,39 +31,50 @@ export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] {
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
}),
execute: async (_id: string, params: Record<string, unknown>) => {
const limit = (params.limit as number) ?? 20;
const limit = Math.min((params.limit as number) ?? 20, 500);
const queryParams = new URLSearchParams();
if (params.subject) queryParams.set("subject", params.subject as string);
if (params.predicate) queryParams.set("predicate", params.predicate as string);
if (params.object) queryParams.set("object", params.object as string);
queryParams.set("limit", String(limit));

const res = await fetch(`${cortexBaseUrl}/api/v1/triples?${queryParams}`);
if (!res.ok) {
return {
content: [{ type: "text" as const, text: `Query failed: ${res.statusText}` }],
};
}
try {
const res = await fetch(`${cortexBaseUrl}/api/v1/triples?${queryParams}`);
if (!res.ok) {
return {
content: [{ type: "text" as const, text: `Query failed: ${res.statusText}` }],
};
}

const data = (await res.json()) as {
triples: Array<{ subject: string; predicate: string; object: unknown }>;
};
if (!data.triples || data.triples.length === 0) {
return { content: [{ type: "text" as const, text: "No triples found." }] };
}
const data = (await res.json()) as {
triples: Array<{ subject: string; predicate: string; object: unknown }>;
};
if (!data.triples || data.triples.length === 0) {
return { content: [{ type: "text" as const, text: "No triples found." }] };
}

const formatted = data.triples
.map((t) => ` ${t.subject} -> ${t.predicate} -> ${JSON.stringify(t.object)}`)
.join("\n");
const formatted = data.triples
.map((t) => ` ${t.subject} -> ${t.predicate} -> ${JSON.stringify(t.object)}`)
.join("\n");

return {
content: [
{
type: "text" as const,
text: `Found ${data.triples.length} triples:\n${formatted}`,
},
],
};
return {
content: [
{
type: "text" as const,
text: `Found ${data.triples.length} triples:\n${formatted}`,
},
],
};
} catch {
return {
content: [
{
type: "text" as const,
text: "Cortex query unavailable. Cortex may not be running.",
},
],
};
}
},
},

Expand All @@ -80,30 +91,41 @@ export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] {
object: Type.String({ description: "Object/value (e.g., 'Express.js')" }),
}),
execute: async (_id: string, params: Record<string, unknown>) => {
const res = await fetch(`${cortexBaseUrl}/api/v1/triples`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
subject: params.subject,
predicate: params.predicate,
object: params.object,
}),
});

if (!res.ok) {
try {
const res = await fetch(`${cortexBaseUrl}/api/v1/triples`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
subject: params.subject,
predicate: params.predicate,
object: params.object,
}),
});

if (!res.ok) {
return {
content: [{ type: "text" as const, text: `Store failed: ${res.statusText}` }],
};
}

return {
content: [{ type: "text" as const, text: `Store failed: ${res.statusText}` }],
content: [
{
type: "text" as const,
text: `Stored: ${params.subject as string} -> ${params.predicate as string} -> ${params.object as string}`,
},
],
};
} catch {
return {
content: [
{
type: "text" as const,
text: "Cortex store unavailable. Cortex may not be running.",
},
],
};
}

return {
content: [
{
type: "text" as const,
text: `Stored: ${params.subject as string} -> ${params.predicate as string} -> ${params.object as string}`,
},
],
};
},
},

Expand Down
50 changes: 40 additions & 10 deletions extensions/mcp-server/governance-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,59 @@ export function createGovernanceTools(): AdaptableTool[] {
const target = params.target as string;

const { readFile, access } = await import("node:fs/promises");
const policyPath = `${process.cwd()}/MAYROS.md`;
const { join } = await import("node:path");
const { homedir } = await import("node:os");

// Search policy file in project dir, then user config
const candidates = [
join(process.cwd(), "MAYROS.md"),
join(homedir(), ".mayros", "MAYROS.md"),
];

let policyContent: string | null = null;
let policyPath = candidates[0];
for (const candidate of candidates) {
try {
await access(candidate);
policyContent = await readFile(candidate, "utf-8");
policyPath = candidate;
break;
} catch {
// Try next candidate
}
}

try {
await access(policyPath);
const content = await readFile(policyPath, "utf-8");
if (!policyContent) {
return {
content: [
{
type: "text" as const,
text: `ALLOWED (no policy): No MAYROS.md found. All actions permitted.`,
},
],
};
}

// Pattern matching against DENY/ALLOW rules
const denyPatterns: string[] = [];
for (const line of content.split("\n")) {
for (const line of policyContent.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("- DENY:")) {
denyPatterns.push(trimmed.slice(7).trim());
}
}

// Check deny rules
// Check deny rules with word-boundary matching
for (const pattern of denyPatterns) {
if (target.includes(pattern) || action.includes(pattern)) {
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`(?:^|[\\s/\\\\.:_-])${escaped}(?:$|[\\s/\\\\.:_-])`, "i");
if (regex.test(target) || regex.test(action)) {
return {
content: [
{
type: "text" as const,
text: `DENIED: "${target}" matches deny rule "${pattern}"`,
text: `DENIED: "${target}" matches deny rule "${pattern}" (from ${policyPath})`,
},
],
};
Expand All @@ -59,16 +89,16 @@ export function createGovernanceTools(): AdaptableTool[] {
content: [
{
type: "text" as const,
text: `ALLOWED: "${action}" on "${target}" — no deny rules matched (${denyPatterns.length} rules checked)`,
text: `ALLOWED: "${action}" on "${target}" — no deny rules matched (${denyPatterns.length} rules checked, from ${policyPath})`,
},
],
};
} catch {
} catch (err) {
return {
content: [
{
type: "text" as const,
text: `ALLOWED (no policy): No MAYROS.md found at ${policyPath}. All actions permitted.`,
text: `Policy check error: ${err instanceof Error ? err.message : String(err)}`,
},
],
};
Expand Down
Loading
Loading