Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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