Skip to content
Open
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
45 changes: 45 additions & 0 deletions src/everything/__tests__/declaration-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { validateDeclarationManifest } from "../server/declaration-manifest.js";

const knownDeclarations = {
tools: new Set<string>(["echo", "get-sum"]),
resources: new Set<string>(["resource-templates"]),
prompts: new Set<string>(["simple-prompt"]),
};

describe("declaration manifest validation", () => {
it("accepts valid declaration manifest", () => {
expect(
validateDeclarationManifest(
JSON.stringify({
tools: ["echo"],
resources: ["resource-templates"],
prompts: ["simple-prompt"],
}),
knownDeclarations
)
).toBeTruthy();
});

it("fails on unknown declaration section", () => {
expect(() =>
validateDeclarationManifest(
JSON.stringify({ unknown: ["value"] }),
knownDeclarations
)
).toThrow("manifest.unknown: unknown declaration section");
});

it("fails on unknown tool/resource/prompt declaration names", () => {
expect(() =>
validateDeclarationManifest(
JSON.stringify({
tools: ["unknown-tool"],
resources: ["unknown-resource"],
prompts: ["unknown-prompt"],
}),
knownDeclarations
)
).toThrow("Declaration manifest validation failed");
});
});
74 changes: 74 additions & 0 deletions src/everything/server/declaration-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
type DeclarationSection = "tools" | "resources" | "prompts";

export type DeclarationManifest = Partial<Record<DeclarationSection, string[]>>;

const DECLARATION_SECTIONS: DeclarationSection[] = [
"tools",
"resources",
"prompts",
];

export function validateDeclarationManifest(
manifestRaw: string | undefined,
knownDeclarations: Record<DeclarationSection, ReadonlySet<string>>
): DeclarationManifest | null {
if (!manifestRaw) {
return null;
}

let parsed: unknown;
try {
parsed = JSON.parse(manifestRaw);
} catch (error) {
throw new Error(
`Invalid declaration manifest JSON: ${(error as Error).message}`
);
}

if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
throw new Error("Declaration manifest must be a JSON object.");
}

const manifest = parsed as Record<string, unknown>;
const errors: string[] = [];

for (const key of Object.keys(manifest)) {
if (!DECLARATION_SECTIONS.includes(key as DeclarationSection)) {
errors.push(`manifest.${key}: unknown declaration section`);
}
}

for (const section of DECLARATION_SECTIONS) {
const value = manifest[section];
if (value === undefined) {
continue;
}

if (!Array.isArray(value)) {
errors.push(`manifest.${section}: expected an array of strings`);
continue;
}

value.forEach((entry, index) => {
if (typeof entry !== "string") {
errors.push(
`manifest.${section}[${index}]: expected string declaration name`
);
return;
}
if (!knownDeclarations[section].has(entry)) {
errors.push(
`manifest.${section}[${index}]: unknown declaration '${entry}'`
);
}
});
}

if (errors.length > 0) {
throw new Error(
`Declaration manifest validation failed:\n${errors.join("\n")}`
);
}

return manifest as DeclarationManifest;
}
36 changes: 36 additions & 0 deletions src/everything/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,37 @@ import { registerResources, readInstructions } from "../resources/index.js";
import { registerPrompts } from "../prompts/index.js";
import { stopSimulatedLogging } from "./logging.js";
import { syncRoots } from "./roots.js";
import { validateDeclarationManifest } from "./declaration-manifest.js";

const KNOWN_EVERYTHING_DECLARATIONS = {
tools: new Set<string>([
"echo",
"get-annotated-message",
"get-env",
"get-resource-links",
"get-resource-reference",
"get-roots-list",
"get-structured-content",
"get-sum",
"get-tiny-image",
"gzip-file-as-resource",
"simulate-research-query",
"toggle-simulated-logging",
"toggle-subscriber-updates",
"trigger-elicitation-request",
"trigger-elicitation-request-async",
"trigger-long-running-operation",
"trigger-sampling-request",
"trigger-sampling-request-async",
]),
resources: new Set<string>(["resource-templates", "file-resources"]),
prompts: new Set<string>([
"simple-prompt",
"args-prompt",
"completable-prompt",
"resource-prompt",
]),
};

// Server Factory response
export type ServerFactoryResponse = {
Expand All @@ -33,6 +64,11 @@ export type ServerFactoryResponse = {
* - `cleanup` {Function}: Function to perform cleanup operations for a closing session.
*/
export const createServer: () => ServerFactoryResponse = () => {
validateDeclarationManifest(
process.env.MCP_DECLARATION_MANIFEST,
KNOWN_EVERYTHING_DECLARATIONS
);

// Read the server instructions
const instructions = readInstructions();

Expand Down
64 changes: 64 additions & 0 deletions src/fetch/src/mcp_server_fetch/server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json
import os
from typing import Annotated, Tuple
from urllib.parse import urlparse, urlunparse

Expand All @@ -24,6 +26,61 @@
DEFAULT_USER_AGENT_MANUAL = "ModelContextProtocol/1.0 (User-Specified; +https://github.com/modelcontextprotocol/servers)"


def validate_declaration_manifest(
manifest_raw: str | None,
*,
known_tools: set[str],
known_resources: set[str],
known_prompts: set[str],
) -> None:
"""Validate declaration manifest and fail closed on unknown entries."""
if not manifest_raw:
return

try:
manifest = json.loads(manifest_raw)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid declaration manifest JSON: {exc}") from exc

if not isinstance(manifest, dict):
raise ValueError("Declaration manifest must be a JSON object.")

sections = {
"tools": known_tools,
"resources": known_resources,
"prompts": known_prompts,
}
errors: list[str] = []

for key in manifest:
if key not in sections:
errors.append(f"manifest.{key}: unknown declaration section")

for section, known in sections.items():
if section not in manifest:
continue
values = manifest[section]
if not isinstance(values, list):
errors.append(f"manifest.{section}: expected an array of strings")
continue

for index, entry in enumerate(values):
if not isinstance(entry, str):
errors.append(
f"manifest.{section}[{index}]: expected string declaration name"
)
continue
if entry not in known:
errors.append(
f"manifest.{section}[{index}]: unknown declaration '{entry}'"
)

if errors:
raise ValueError(
"Declaration manifest validation failed:\n" + "\n".join(errors)
)


def extract_content_from_html(html: str) -> str:
"""Extract and convert HTML content to Markdown format.

Expand Down Expand Up @@ -190,6 +247,13 @@ async def serve(
ignore_robots_txt: Whether to ignore robots.txt restrictions
proxy_url: Optional proxy URL to use for requests
"""
validate_declaration_manifest(
os.getenv("MCP_DECLARATION_MANIFEST"),
known_tools={"fetch"},
known_resources=set(),
known_prompts={"fetch"},
)

server = Server("mcp-fetch")
user_agent_autonomous = custom_user_agent or DEFAULT_USER_AGENT_AUTONOMOUS
user_agent_manual = custom_user_agent or DEFAULT_USER_AGENT_MANUAL
Expand Down
29 changes: 29 additions & 0 deletions src/fetch/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
check_may_autonomously_fetch_url,
fetch_url,
DEFAULT_USER_AGENT_AUTONOMOUS,
validate_declaration_manifest,
)


Expand Down Expand Up @@ -219,6 +220,34 @@ async def test_fetch_html_page(self):
assert isinstance(content, str)
assert prefix == ""


class TestDeclarationManifestValidation:
def test_accepts_valid_manifest(self):
validate_declaration_manifest(
'{"tools":["fetch"],"prompts":["fetch"],"resources":[]}',
known_tools={"fetch"},
known_resources=set(),
known_prompts={"fetch"},
)

def test_rejects_unknown_declaration_name(self):
with pytest.raises(ValueError, match="manifest.tools\\[0\\]: unknown declaration 'invalid'"):
validate_declaration_manifest(
'{"tools":["invalid"]}',
known_tools={"fetch"},
known_resources=set(),
known_prompts={"fetch"},
)

def test_rejects_unknown_declaration_section(self):
with pytest.raises(ValueError, match="manifest.bad: unknown declaration section"):
validate_declaration_manifest(
'{"bad":["fetch"]}',
known_tools={"fetch"},
known_resources=set(),
known_prompts={"fetch"},
)

@pytest.mark.asyncio
async def test_fetch_html_page_raw(self):
"""Test fetching an HTML page with raw=True returns original HTML."""
Expand Down
37 changes: 37 additions & 0 deletions src/filesystem/__tests__/declaration-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { validateDeclarationManifest } from "../declaration-manifest.js";

const knownDeclarations = {
tools: new Set<string>(["read_text_file", "write_file"]),
resources: new Set<string>(),
prompts: new Set<string>(),
};

describe("filesystem declaration manifest validation", () => {
it("accepts valid tool declarations", () => {
expect(() =>
validateDeclarationManifest(
JSON.stringify({ tools: ["read_text_file"] }),
knownDeclarations
)
).not.toThrow();
});

it("fails on unknown declaration section", () => {
expect(() =>
validateDeclarationManifest(
JSON.stringify({ unknown: ["x"] }),
knownDeclarations
)
).toThrow("manifest.unknown: unknown declaration section");
});

it("fails on unknown declaration name", () => {
expect(() =>
validateDeclarationManifest(
JSON.stringify({ tools: ["not-a-tool"] }),
knownDeclarations
)
).toThrow("manifest.tools[0]: unknown declaration 'not-a-tool'");
});
});
Loading