diff --git a/src/langbot/pkg/entity/persistence/mcp.py b/src/langbot/pkg/entity/persistence/mcp.py index e9eedbdb..31348fce 100644 --- a/src/langbot/pkg/entity/persistence/mcp.py +++ b/src/langbot/pkg/entity/persistence/mcp.py @@ -11,6 +11,10 @@ class MCPServer(Base): enable = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) mode = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) # stdio, sse, http extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={}) + # Markdown documentation captured from LangBot Space at install time so the + # detail page can show docs even when the server is offline / has no tools. + # Empty string for manually-created servers that have no marketplace README. + readme = sqlalchemy.Column(sqlalchemy.Text, nullable=False, server_default='', default='') created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) updated_at = sqlalchemy.Column( sqlalchemy.DateTime, diff --git a/src/langbot/pkg/persistence/alembic/versions/0004_add_mcp_readme.py b/src/langbot/pkg/persistence/alembic/versions/0004_add_mcp_readme.py new file mode 100644 index 00000000..c845ddd3 --- /dev/null +++ b/src/langbot/pkg/persistence/alembic/versions/0004_add_mcp_readme.py @@ -0,0 +1,34 @@ +"""add readme column to mcp_servers + +Revision ID: 0004_add_mcp_readme +Revises: 0003_add_rerank_models +Create Date: 2026-06-06 +""" + +import sqlalchemy as sa +from alembic import op + +revision = '0004_add_mcp_readme' +down_revision = '0003_add_rerank_models' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add ``readme`` to mcp_servers if the table exists and the column is missing + # (the table may have been created by create_all() with the column already + # present on fresh installs, so guard against duplicate-add). + conn = op.get_bind() + inspector = sa.inspect(conn) + if 'mcp_servers' not in inspector.get_table_names(): + return + columns = {col['name'] for col in inspector.get_columns('mcp_servers')} + if 'readme' not in columns: + op.add_column( + 'mcp_servers', + sa.Column('readme', sa.Text(), nullable=False, server_default=''), + ) + + +def downgrade() -> None: + op.drop_column('mcp_servers', 'readme') diff --git a/src/langbot/pkg/plugin/connector.py b/src/langbot/pkg/plugin/connector.py index 54fd382f..2bb9088d 100644 --- a/src/langbot/pkg/plugin/connector.py +++ b/src/langbot/pkg/plugin/connector.py @@ -248,6 +248,9 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): mode = mcp_data.get('mode') or 'stdio' extra_args = mcp_data.get('extra_args') or {} + # Marketplace records carry the rendered README markdown; persist it so + # the detail page Docs tab works offline and without a marketplace round-trip. + readme = mcp_data.get('readme') or '' # Use __ instead of / to avoid URL routing issues with slashes name = f'{mcp_data.get("author", "")}__{mcp_data.get("name", "")}' @@ -267,6 +270,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): 'enable': True, 'mode': mode, 'extra_args': extra_args, + 'readme': readme, } await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data)) diff --git a/web/src/app/home/mcp/components/mcp-form/MCPReadme.tsx b/web/src/app/home/mcp/components/mcp-form/MCPReadme.tsx new file mode 100644 index 00000000..9b213001 --- /dev/null +++ b/web/src/app/home/mcp/components/mcp-form/MCPReadme.tsx @@ -0,0 +1,74 @@ +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; +import rehypeSanitize from 'rehype-sanitize'; +import rehypeHighlight from 'rehype-highlight'; +import rehypeSlug from 'rehype-slug'; +import rehypeAutolinkHeadings from 'rehype-autolink-headings'; +import { useTranslation } from 'react-i18next'; +import '@/styles/github-markdown.css'; + +/** + * Renders the README markdown captured from LangBot Space at install time. + * The README is stored on the MCP server record (``server.readme``) so this + * works offline and regardless of the server's runtime/connection state. + * + * MCP marketplace READMEs reference images by absolute URL (the upstream repo), + * so — unlike plugin READMEs — no asset-path rewriting is needed here. + */ +export default function MCPReadme({ readme }: { readme?: string }) { + const { t } = useTranslation(); + + if (!readme || !readme.trim()) { + return ( +
+ {t('mcp.noReadme')} +
+ ); + } + + return ( +
+
+
    {children}
, + ol: ({ children }) =>
    {children}
, + li: ({ children }) =>
  • {children}
  • , + a: ({ children, href, ...props }) => ( + + {children} + + ), + img: ({ src, alt, ...props }) => ( + {alt + ), + }} + > + {readme} +
    +
    +
    + ); +} diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index 171b4d47..44edd872 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -551,6 +551,7 @@ export type MCPServer = enable: boolean; extra_args: MCPServerExtraArgsSSE; runtime_info?: MCPServerRuntimeInfo; + readme?: string; created_at?: string; updated_at?: string; } @@ -561,6 +562,7 @@ export type MCPServer = enable: boolean; extra_args: MCPServerExtraArgsHttp; runtime_info?: MCPServerRuntimeInfo; + readme?: string; created_at?: string; updated_at?: string; } @@ -571,6 +573,7 @@ export type MCPServer = enable: boolean; extra_args: MCPServerExtraArgsStdio; runtime_info?: MCPServerRuntimeInfo; + readme?: string; created_at?: string; updated_at?: string; };