Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5
This commit is contained in:
commit
5aa3e8d685
700 files changed
+132801
No files matched your search
@@ -0,0 +1,239 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import type { AppEnv } from "../config/env.js";
|
||||
import { assertReadablePath } from "../integrations/gitea/policy.js";
|
||||
import {
|
||||
listPortfolioRepositories,
|
||||
parsePortfolioFetchId,
|
||||
portfolioFetchId,
|
||||
type PortfolioRepository,
|
||||
} from "../integrations/gitea/portfolio.js";
|
||||
import { getGiteaClient, recordMcpAudit } from "../integrations/gitea/service.js";
|
||||
import type { GiteaContent } from "../integrations/gitea/types.js";
|
||||
|
||||
type ToolResult = {
|
||||
content: Array<{ type: "text"; text: string }>;
|
||||
structuredContent?: Record<string, unknown>;
|
||||
isError?: boolean;
|
||||
};
|
||||
|
||||
const asResult = (data: unknown, message = "Operation completed"): ToolResult => ({
|
||||
content: [{ type: "text", text: `${message}\n${JSON.stringify(data, null, 2)}` }],
|
||||
structuredContent:
|
||||
data && typeof data === "object" && !Array.isArray(data)
|
||||
? (data as Record<string, unknown>)
|
||||
: { data },
|
||||
});
|
||||
|
||||
const asKnowledgeResult = (data: Record<string, unknown>): ToolResult => ({
|
||||
content: [{ type: "text", text: JSON.stringify(data) }],
|
||||
});
|
||||
|
||||
const decodeFile = (content: GiteaContent) => {
|
||||
if (
|
||||
content.type !== "file" ||
|
||||
content.encoding !== "base64" ||
|
||||
typeof content.content !== "string"
|
||||
) {
|
||||
throw new Error("GITEA_CONTENT_NOT_FILE");
|
||||
}
|
||||
return Buffer.from(content.content.replace(/\s/g, ""), "base64").toString("utf8");
|
||||
};
|
||||
|
||||
const mapBounded = async <T, R>(
|
||||
items: T[],
|
||||
limit: number,
|
||||
operation: (item: T) => Promise<R>,
|
||||
): Promise<R[]> => {
|
||||
const output: R[] = [];
|
||||
for (let index = 0; index < items.length; index += limit) {
|
||||
output.push(...(await Promise.all(items.slice(index, index + limit).map(operation))));
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const repositoryKey = (repository: Pick<PortfolioRepository, "owner" | "repository">) =>
|
||||
`${repository.owner}/${repository.repository}`.toLowerCase();
|
||||
|
||||
const errorCode = (error: unknown) =>
|
||||
error instanceof Error ? error.message : "MCP_PORTFOLIO_OPERATION_FAILED";
|
||||
|
||||
const auditedRead = async (
|
||||
actor: string,
|
||||
toolName: string,
|
||||
operation: () => Promise<ToolResult>,
|
||||
context: { repository?: string; branch?: string } = {},
|
||||
): Promise<ToolResult> => {
|
||||
const started = Date.now();
|
||||
try {
|
||||
const result = await operation();
|
||||
await recordMcpAudit({
|
||||
actor,
|
||||
toolName,
|
||||
repository: context.repository,
|
||||
branch: context.branch,
|
||||
outcome: "succeeded",
|
||||
durationMs: Date.now() - started,
|
||||
metadata: { projection: "gitea-portfolio", readOnly: true },
|
||||
}).catch(() => undefined);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const code = errorCode(error);
|
||||
await recordMcpAudit({
|
||||
actor,
|
||||
toolName,
|
||||
repository: context.repository,
|
||||
branch: context.branch,
|
||||
outcome: "failed",
|
||||
errorCode: code,
|
||||
durationMs: Date.now() - started,
|
||||
metadata: { projection: "gitea-portfolio", readOnly: true },
|
||||
}).catch(() => undefined);
|
||||
return {
|
||||
content: [{ type: "text", text: `Operation failed: ${code}` }],
|
||||
structuredContent: { error: code },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register cross-repository search and fetch tools without changing the
|
||||
* existing portfolio catalog or the GiteaRepositoryLink ownership/write
|
||||
* boundary used by ProjectBrain delivery.
|
||||
*/
|
||||
export const registerPortfolioMcpTools = (
|
||||
server: McpServer,
|
||||
env: AppEnv,
|
||||
actor: string,
|
||||
) => {
|
||||
const client = getGiteaClient(env);
|
||||
|
||||
server.registerTool(
|
||||
"search_portfolio_repositories",
|
||||
{
|
||||
title: "Search Gitea portfolio repositories",
|
||||
description:
|
||||
"Search code across every repository visible to ProjectBrain's scoped Gitea identity. Returned gitea-repository:// ids can be read with fetch_portfolio_file without creating ProjectBrain ownership links.",
|
||||
inputSchema: {
|
||||
query: z.string().trim().min(2).max(200).describe("Plain-text code search query"),
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
},
|
||||
async ({ query }) =>
|
||||
auditedRead(actor, "search_portfolio_repositories", async () => {
|
||||
const repositories = await listPortfolioRepositories(env);
|
||||
const searched = repositories.filter((repository) => !repository.archived);
|
||||
const batches = await mapBounded(searched, 6, async (repository) => {
|
||||
try {
|
||||
const response = await client.searchCode(
|
||||
repository.owner,
|
||||
repository.repository,
|
||||
query,
|
||||
repository.defaultBranch,
|
||||
);
|
||||
const rows = Array.isArray(response) ? response : response.data ?? [];
|
||||
return {
|
||||
results: rows.map((row) => ({
|
||||
id: portfolioFetchId(repository, repository.defaultBranch, row.path),
|
||||
title: `${repository.fullName}: ${row.path}`,
|
||||
url: row.html_url,
|
||||
metadata: {
|
||||
repository: repository.fullName,
|
||||
ref: repository.defaultBranch,
|
||||
path: row.path,
|
||||
linked: repository.linked,
|
||||
projectId: repository.link?.projectSlug,
|
||||
},
|
||||
})),
|
||||
};
|
||||
} catch {
|
||||
return { results: [], failedRepository: repository.fullName };
|
||||
}
|
||||
});
|
||||
const allResults = batches.flatMap((batch) => batch.results);
|
||||
const failedRepositories = batches
|
||||
.map((batch) => batch.failedRepository)
|
||||
.filter((value): value is string => typeof value === "string");
|
||||
return asKnowledgeResult({
|
||||
results: allResults.slice(0, 50),
|
||||
metadata: {
|
||||
repositoryCount: repositories.length,
|
||||
searchedRepositoryCount: searched.length,
|
||||
failedRepositories,
|
||||
truncated: allResults.length > 50,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"fetch_portfolio_file",
|
||||
{
|
||||
title: "Fetch Gitea portfolio file",
|
||||
description:
|
||||
"Read one text file by the canonical gitea-repository:// id returned by search_portfolio_repositories. The repository must still be visible to ProjectBrain's scoped Gitea identity and sensitive paths remain blocked.",
|
||||
inputSchema: {
|
||||
id: z
|
||||
.string()
|
||||
.startsWith("gitea-repository://")
|
||||
.max(2_000)
|
||||
.describe("Canonical gitea-repository://owner/repository/ref/path identifier"),
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
},
|
||||
async ({ id }) => {
|
||||
let parsed: ReturnType<typeof parsePortfolioFetchId>;
|
||||
try {
|
||||
parsed = parsePortfolioFetchId(id);
|
||||
} catch (error) {
|
||||
return asResult({ error: errorCode(error) }, "Invalid portfolio fetch id");
|
||||
}
|
||||
|
||||
return auditedRead(
|
||||
actor,
|
||||
"fetch_portfolio_file",
|
||||
async () => {
|
||||
const repositories = await listPortfolioRepositories(env);
|
||||
const visible = new Map(repositories.map((repository) => [repositoryKey(repository), repository]));
|
||||
const repository = visible.get(`${parsed.owner}/${parsed.repository}`.toLowerCase());
|
||||
if (!repository) throw new Error("GITEA_REPOSITORY_NOT_VISIBLE");
|
||||
const path = assertReadablePath(parsed.path);
|
||||
const file = await client.getContents(
|
||||
repository.owner,
|
||||
repository.repository,
|
||||
path,
|
||||
parsed.ref,
|
||||
);
|
||||
if (Array.isArray(file)) throw new Error("GITEA_CONTENT_NOT_FILE");
|
||||
return asKnowledgeResult({
|
||||
id,
|
||||
title: `${repository.fullName}: ${path}`,
|
||||
text: decodeFile(file),
|
||||
url: file.html_url,
|
||||
metadata: {
|
||||
repository: repository.fullName,
|
||||
path,
|
||||
ref: parsed.ref,
|
||||
sha: file.sha,
|
||||
size: file.size,
|
||||
linked: repository.linked,
|
||||
projectId: repository.link?.projectSlug,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ repository: `${parsed.owner}/${parsed.repository}`, branch: parsed.ref },
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user