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
9 changes: 9 additions & 0 deletions frontend/src/components/app-config/ai-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { SettingSubtitle } from "./common";
import { AWS_REGIONS } from "./constants";
import { IncorrectModelId } from "./incorrect-model-id";
import { IsOverridden } from "./is-overridden";
import { MCPConfig } from "./mcp-config";

const formItemClasses = "flex flex-row items-center space-x-1 space-y-0";

Expand Down Expand Up @@ -1364,12 +1365,15 @@ export const AiConfig: React.FC<AiConfigProps> = ({
config,
onSubmit,
}) => {
// MCP is not supported in WASM
const wasm = isWasm();
return (
<Tabs defaultValue="ai-features" className="flex-1">
<TabsList className="mb-2">
<TabsTrigger value="ai-features">AI Features</TabsTrigger>
<TabsTrigger value="ai-providers">AI Providers</TabsTrigger>
<TabsTrigger value="ai-models">AI Models</TabsTrigger>
{!wasm && <TabsTrigger value="mcp">MCP</TabsTrigger>}
</TabsList>

<TabsContent value="ai-features">
Expand All @@ -1386,6 +1390,11 @@ export const AiConfig: React.FC<AiConfigProps> = ({
<TabsContent value="ai-models">
<AiModelDisplayConfig form={form} config={config} onSubmit={onSubmit} />
</TabsContent>
{!wasm && (
<TabsContent value="mcp">
<MCPConfig form={form} onSubmit={onSubmit} />
</TabsContent>
)}
</Tabs>
);
};
128 changes: 128 additions & 0 deletions frontend/src/components/app-config/mcp-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* Copyright 2024 Marimo. All rights reserved. */

import { CheckSquareIcon } from "lucide-react";
import React from "react";
import type { UseFormReturn } from "react-hook-form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { FormField, FormItem } from "@/components/ui/form";
import type { UserConfig } from "@/core/config/config-schema";
import { Button } from "../ui/button";
import { Kbd } from "../ui/kbd";
import { SettingSubtitle } from "./common";
import { useOpenSettingsToTab } from "./state";

interface MCPConfigProps {
form: UseFormReturn<UserConfig>;
onSubmit: (values: UserConfig) => void;
}

type MCPPreset = "marimo" | "context7";

interface PresetConfig {
id: MCPPreset;
title: string;
description: string;
}

const PRESET_CONFIGS: PresetConfig[] = [
{
id: "marimo",
title: "marimo (docs)",
description: "Access marimo documentation",
},
{
id: "context7",
title: "Context7",
description: "Connect to Context7 MCP server",
},
];

export const MCPConfig: React.FC<MCPConfigProps> = ({ form, onSubmit }) => {
const { handleClick } = useOpenSettingsToTab();

return (
<div className="flex flex-col gap-4">
<SettingSubtitle>MCP Servers</SettingSubtitle>
<p className="text-sm text-muted-foreground">
Enable Model Context Protocol (MCP) servers to provide additional
capabilities and data sources for AI features.
</p>
<p className="text-sm text-muted-foreground">
This feature requires the <Kbd className="inline">marimo[mcp]</Kbd>{" "}
package. See{" "}
<Button
variant="link"
onClick={() => handleClick("optionalDeps")}
size="xs"
>
Optional Features
</Button>{" "}
for more details.
</p>

<FormField
control={form.control}
name="mcp.presets"
render={({ field }) => {
const presets = field.value || [];

const togglePreset = (preset: MCPPreset) => {
const newPresets = presets.includes(preset)
? presets.filter((p: string) => p !== preset)
: [...presets, preset];
field.onChange(newPresets);
onSubmit(form.getValues());
};

return (
<FormItem>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{PRESET_CONFIGS.map((config) => {
const isChecked = presets.includes(config.id);

return (
<Card
key={config.id}
className={`cursor-pointer transition-all ${
isChecked
? "border-[var(--blue-9)] bg-[var(--blue-2)]"
: "hover:border-[var(--blue-7)]"
}`}
onClick={() => togglePreset(config.id)}
>
<CardHeader>
<div className="flex items-start justify-between">
<CardTitle className="text-base">
{config.title}
</CardTitle>
<span
className={`h-5 w-5 flex items-center justify-center rounded border ${
isChecked
? "border-[var(--blue-7)] bg-[var(--blue-7)] text-foreground"
: "border-muted bg-background text-muted-foreground"
}`}
>
{isChecked ? <CheckSquareIcon /> : null}
</span>
</div>
</CardHeader>
<CardContent>
<CardDescription>{config.description}</CardDescription>
</CardContent>
</Card>
);
})}
</div>
</FormItem>
);
}}
/>
</div>
);
};
6 changes: 6 additions & 0 deletions frontend/src/components/app-config/optional-features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ const OPTIONAL_DEPENDENCIES: OptionalFeature[] = [
additionalPackageInstalls: [],
description: "AI features",
},
{
id: "mcp",
packagesRequired: [{ name: "mcp", minVersion: "1" }],
additionalPackageInstalls: [{ name: "pydantic", minVersion: "2" }],
description: "Connect to MCP servers",
},
{
id: "ipy-export",
packagesRequired: [{ name: "nbformat" }],
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/chat/tool-call-accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const ResultRenderer: React.FC<{ result: unknown }> = ({ result }) => {

// Otherwise, fall back to the current JSON viewer
return (
<div className="text-xs font-medium text-muted-foreground mb-1">
<div className="text-xs font-medium text-muted-foreground mb-1 max-h-64 overflow-y-auto scrollbar-thin">
{typeof result === "string" ? result : JSON.stringify(result, null, 2)}
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/core/config/__tests__/config-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ test("default UserConfig - empty", () => {
"overrides": {},
"preset": "default",
},
"mcp": {},
"package_management": {
"manager": "pip",
},
Expand Down Expand Up @@ -139,6 +140,7 @@ test("default UserConfig - one level", () => {
"overrides": {},
"preset": "default",
},
"mcp": {},
"package_management": {
"manager": "pip",
},
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/core/config/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ export const UserConfigSchema = z
wasm: z.boolean().optional(),
})
.optional(),
mcp: z
.looseObject({
presets: z.array(z.enum(["marimo", "context7"])).optional(),
})
.optional()
.prefault({}),
})
.partial()
.prefault(() => ({
Expand All @@ -201,6 +207,7 @@ export const UserConfigSchema = z
server: {},
ai: {},
package_management: {},
mcp: {},
}));
export type UserConfig = MarimoConfig;
export type SaveConfig = UserConfig["save"];
Expand Down Expand Up @@ -302,6 +309,7 @@ export function defaultUserConfig(): UserConfig {
server: {},
ai: {},
package_management: {},
mcp: {},
};
return UserConfigSchema.parse(defaultConfig) as UserConfig;
}
2 changes: 0 additions & 2 deletions frontend/src/core/config/feature-flag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export interface ExperimentalFeatures {
wasm_layouts: boolean; // Used in playground (community cloud)
rtc_v2: boolean;
performant_table_charts: boolean;
mcp_docs: boolean;
chat_modes: boolean;
sql_linter: boolean;
external_agents: boolean;
Expand All @@ -25,7 +24,6 @@ const defaultValues: ExperimentalFeatures = {
wasm_layouts: false,
rtc_v2: false,
performant_table_charts: false,
mcp_docs: false,
chat_modes: false,
sql_linter: true,
external_agents: import.meta.env.DEV,
Expand Down
27 changes: 13 additions & 14 deletions marimo/_config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import NotRequired

from typing import (
TYPE_CHECKING,
Any,
Literal,
Optional,
Expand Down Expand Up @@ -508,7 +509,6 @@ class ExperimentalConfig(TypedDict, total=False):
wasm_layouts: bool # Used in playground (community cloud)
rtc_v2: bool
performant_table_charts: bool
mcp_docs: bool
chat_modes: bool
sql_linter: bool
sql_mode: bool
Expand Down Expand Up @@ -543,8 +543,7 @@ class MarimoConfig(TypedDict):
snippets: NotRequired[SnippetsConfig]
datasources: NotRequired[DatasourcesConfig]
sharing: NotRequired[SharingConfig]
# We don't support configuring MCP servers yet
# mcp: NotRequired[MCPConfig]
mcp: NotRequired[MCPConfig]


@mddoc
Expand All @@ -570,7 +569,12 @@ class MCPServerStreamableHttpConfig(TypedDict):
disabled: NotRequired[Optional[bool]]


MCPServerConfig = Union[MCPServerStdioConfig, MCPServerStreamableHttpConfig]
if TYPE_CHECKING:
MCPServerConfig = Union[
MCPServerStdioConfig, MCPServerStreamableHttpConfig
]
else:
MCPServerConfig = dict[str, Any]


@mddoc
Expand All @@ -584,16 +588,7 @@ class MCPConfig(TypedDict):
"""

mcpServers: dict[str, MCPServerConfig]


DEFAULT_MCP_CONFIG: MCPConfig = MCPConfig(
mcpServers={
"marimo": MCPServerStreamableHttpConfig(
url="https://mcp.marimo.app/mcp"
),
# TODO(bjoaquinc): add more Marimo MCP servers here after they are implemented
}
)
presets: NotRequired[list[Literal["marimo", "context7"]]]


@mddoc
Expand Down Expand Up @@ -677,6 +672,10 @@ class PartialMarimoConfig(TypedDict, total=False):
"custom_paths": [],
"include_default_snippets": True,
},
"mcp": {
"mcpServers": {},
"presets": [],
},
}


Expand Down
48 changes: 48 additions & 0 deletions marimo/_server/ai/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2024 Marimo. All rights reserved.
"""MCP (Model Context Protocol) client implementation for marimo."""

from marimo._server.ai.mcp.client import (
MCPClient,
MCPServerConnection,
MCPServerStatus,
get_mcp_client,
)
from marimo._server.ai.mcp.config import (
MCP_PRESETS,
MCPConfigComparator,
MCPConfigDiff,
MCPServerDefinition,
MCPServerDefinitionFactory,
append_presets,
)
from marimo._server.ai.mcp.transport import (
MCPTransportConnector,
MCPTransportRegistry,
MCPTransportType,
StdioTransportConnector,
StreamableHTTPTransportConnector,
)
from marimo._server.ai.mcp.types import MCPToolArgs

__all__ = [
# Client classes
"MCPClient",
"MCPServerConnection",
"MCPServerStatus",
"get_mcp_client",
# Config classes
"MCP_PRESETS",
"MCPConfigComparator",
"MCPConfigDiff",
"MCPServerDefinition",
"MCPServerDefinitionFactory",
"append_presets",
# Transport classes
"MCPTransportConnector",
"MCPTransportRegistry",
"MCPTransportType",
"StdioTransportConnector",
"StreamableHTTPTransportConnector",
# Types
"MCPToolArgs",
]
Loading
Loading