Skip to content

MCP Server

lcp.mcp_server

MCP server that exposes LCP manifest data to AI agents.

LCPIndex

In-memory index of LCP document for fast lookups.

Source code in src/lcp/mcp_server.py
class LCPIndex:
    """In-memory index of LCP document for fast lookups."""

    def __init__(self, doc: LCPDocument):
        self.doc = doc
        self.symbols_by_id: dict[str, Symbol] = doc.symbols
        self.symbols_by_module: dict[str, list[str]] = defaultdict(list)
        self.symbols_by_kind: dict[str, list[str]] = defaultdict(list)
        self.class_members: dict[str, list[str]] = defaultdict(list)
        self.modules: set[str] = set()

        self._build_indexes()

    def _build_indexes(self) -> None:
        """Build lookup indexes from the LCP document."""
        for symbol_id, symbol in self.symbols_by_id.items():
            # Index by module
            if symbol.module:
                self.symbols_by_module[symbol.module].append(symbol_id)
                self.modules.add(symbol.module)

            # Index by kind
            self.symbols_by_kind[symbol.kind.value].append(symbol_id)

            # Index class members (symbols with # in ID belong to a class)
            if "#" in symbol_id:
                class_id = symbol_id.split("#")[0]
                self.class_members[class_id].append(symbol_id)

MultiLibraryIndex

Manages multiple LCPIndex instances for a universal MCP server.

Holds one LCPIndex per loaded library, keyed by library name. Tracks the most recently resolved library as the implicit default.

Source code in src/lcp/mcp_server.py
class MultiLibraryIndex:
    """Manages multiple LCPIndex instances for a universal MCP server.

    Holds one LCPIndex per loaded library, keyed by library name.
    Tracks the most recently resolved library as the implicit default.
    """

    def __init__(self) -> None:
        self._indexes: dict[str, LCPIndex] = {}
        self._default: str | None = None

    @property
    def default_library(self) -> str | None:
        """Most recently resolved library name (used as implicit default)."""
        return self._default

    def add(self, name: str, index: LCPIndex) -> None:
        """Register a library index."""
        self._indexes[name] = index
        self._default = name

    def get(self, name: str | None = None) -> LCPIndex | None:
        """Return the index for *name*, or the default index if *name* is None."""
        key = name if name is not None else self._default
        if key is None:
            return None
        return self._indexes.get(key)

    def list_libraries(self) -> list[dict[str, Any]]:
        """Return summary info for all loaded libraries."""
        result = []
        for name, idx in self._indexes.items():
            lib = idx.doc.manifest.library
            result.append(
                {
                    "name": name,
                    "version": lib.version,
                    "language": lib.language,
                    "symbol_count": len(idx.symbols_by_id),
                    "is_default": name == self._default,
                }
            )
        return result

    def __contains__(self, name: str) -> bool:
        return name in self._indexes

default_library property

default_library: str | None

Most recently resolved library name (used as implicit default).

add

add(name: str, index: LCPIndex) -> None

Register a library index.

Source code in src/lcp/mcp_server.py
def add(self, name: str, index: LCPIndex) -> None:
    """Register a library index."""
    self._indexes[name] = index
    self._default = name

get

get(name: str | None = None) -> LCPIndex | None

Return the index for name, or the default index if name is None.

Source code in src/lcp/mcp_server.py
def get(self, name: str | None = None) -> LCPIndex | None:
    """Return the index for *name*, or the default index if *name* is None."""
    key = name if name is not None else self._default
    if key is None:
        return None
    return self._indexes.get(key)

list_libraries

list_libraries() -> list[dict[str, Any]]

Return summary info for all loaded libraries.

Source code in src/lcp/mcp_server.py
def list_libraries(self) -> list[dict[str, Any]]:
    """Return summary info for all loaded libraries."""
    result = []
    for name, idx in self._indexes.items():
        lib = idx.doc.manifest.library
        result.append(
            {
                "name": name,
                "version": lib.version,
                "language": lib.language,
                "symbol_count": len(idx.symbols_by_id),
                "is_default": name == self._default,
            }
        )
    return result

load_lcp_document

load_lcp_document(path: str | Path) -> LCPDocument

Load and validate an LCP document from a file.

Supports both plain .lcp.json files and gzip-compressed .lcp.json.gz files, detected transparently by file extension.

Source code in src/lcp/mcp_server.py
def load_lcp_document(path: str | Path) -> LCPDocument:
    """Load and validate an LCP document from a file.

    Supports both plain ``.lcp.json`` files and gzip-compressed
    ``.lcp.json.gz`` files, detected transparently by file extension.
    """
    import gzip as _gzip

    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"LCP file not found: {path}")

    if path.suffix == ".gz":
        with _gzip.open(path, "rb") as f:
            data = json.loads(f.read())
    else:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)

    return LCPDocument.model_validate(data)

resolve_library_document

resolve_library_document(name: str, cache_dir: Path = _DEFAULT_CACHE_DIR, no_cache: bool = False, registry_url: str | None = None) -> tuple[LCPDocument, str]

Resolve an LCP document for name using the standard resolution order.

Resolution order
  1. Local cache (~/.lcp/cache/{name}/{version}.lcp.json)
  2. Live scan (package is pip-installed)
  3. Registry (HTTP GET from registry_url if provided)
  4. Error

Registry manifests are fetched from the path {registry_url}/manifests/python/{name}/{version}.lcp.json. When the installed version is unknown, "latest" is used as the version segment so registries can expose a canonical latest entry.

Parameters:

Name Type Description Default
name str

Python package name to resolve.

required
cache_dir Path

Cache root directory (default: ~/.lcp/cache/).

_DEFAULT_CACHE_DIR
no_cache bool

Skip cache read/write entirely.

False
registry_url str | None

Optional base URL of an LCP registry to try when local scanning fails. The default official registry is at https://raw.githubusercontent.com/zazza123/lcp-registry/refs/heads/main.

None

Returns:

Type Description
LCPDocument

Tuple of (LCPDocument, source) where source is "cache",

str

"scan", or "registry".

Raises:

Type Description
ImportError

If the package cannot be resolved via any available source (cache, scan, or registry).

Source code in src/lcp/mcp_server.py
def resolve_library_document(
    name: str,
    cache_dir: Path = _DEFAULT_CACHE_DIR,
    no_cache: bool = False,
    registry_url: str | None = None,
) -> tuple[LCPDocument, str]:
    """Resolve an LCP document for *name* using the standard resolution order.

    Resolution order:
      1. Local cache  (~/.lcp/cache/{name}/{version}.lcp.json)
      2. Live scan    (package is pip-installed)
      3. Registry     (HTTP GET from *registry_url* if provided)
      4. Error

    Registry manifests are fetched from the path
    ``{registry_url}/manifests/python/{name}/{version}.lcp.json``.
    When the installed version is unknown, ``"latest"`` is used as the
    version segment so registries can expose a canonical latest entry.

    Args:
        name: Python package name to resolve.
        cache_dir: Cache root directory (default: ~/.lcp/cache/).
        no_cache: Skip cache read/write entirely.
        registry_url: Optional base URL of an LCP registry to try when local
            scanning fails.  The default official registry is at
            ``https://raw.githubusercontent.com/zazza123/lcp-registry/refs/heads/main``.

    Returns:
        Tuple of (LCPDocument, source) where source is ``"cache"``,
        ``"scan"``, or ``"registry"``.

    Raises:
        ImportError: If the package cannot be resolved via any available
            source (cache, scan, or registry).
    """
    from .scanner import scan_package
    from .generator import generate_lcp

    # Resolve the installed version once; used for both cache lookup and registry fetch
    installed_ver = _installed_version(name)

    # 1. Cache lookup
    if not no_cache:
        if installed_ver:
            # If the package has a known installed version, look for an exact match
            cached = _load_from_cache(cache_dir, name, installed_ver)
            if cached is not None:
                return cached, "cache"
        else:
            # No installed version metadata: return any cached entry for this package
            cached = _find_any_cached(cache_dir, name)
            if cached is not None:
                return cached, "cache"

    # 2. Live scan
    scan_error: Exception | None = None
    try:
        scanned = scan_package(name, include_private=False, recursive=True)
        doc = generate_lcp(scanned)
        if not no_cache:
            try:
                _save_to_cache(cache_dir, doc)
            except Exception:
                pass  # cache write failure is non-fatal
        return doc, "scan"
    except Exception as exc:
        scan_error = exc

    # 3. Registry fallback
    if registry_url:
        try:
            doc = _fetch_from_registry(name, registry_url, version=installed_ver)
            if not no_cache:
                try:
                    _save_to_cache(cache_dir, doc)
                except Exception:
                    pass  # cache write failure is non-fatal
            return doc, "registry"
        except ImportError:
            pass  # fall through to final error

    raise ImportError(
        f"Cannot resolve library '{name}': not installed or scan failed. "
        f"Install it first with: pip install {name}"
        + (f" (registry fetch also failed: {registry_url})" if registry_url else "")
    ) from scan_error

create_server

create_server(manifest_path: str | Path, name: str | None = None) -> FastMCP

Create an MCP server for the given LCP manifest.

Parameters:

Name Type Description Default
manifest_path str | Path

Path to the .lcp.json file

required
name str | None

Server name (default: lcp-{library-name})

None

Returns:

Type Description
FastMCP

Configured FastMCP server instance

Source code in src/lcp/mcp_server.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
def create_server(
    manifest_path: str | Path,
    name: str | None = None,
) -> FastMCP:
    """Create an MCP server for the given LCP manifest.

    Args:
        manifest_path: Path to the .lcp.json file
        name: Server name (default: lcp-{library-name})

    Returns:
        Configured FastMCP server instance
    """
    doc = load_lcp_document(manifest_path)
    index = LCPIndex(doc)

    if name is None:
        name = f"lcp-{doc.manifest.library.name}"

    mcp = FastMCP(name)

    @mcp.tool()
    def get_usage_guide() -> dict[str, Any]:
        """Get strategic guidance on how to efficiently use this LCP manifest.

        CALL THIS FIRST to understand the recommended workflow for exploring
        this library and avoiding common mistakes.

        Returns:
            Recommended workflow, cost optimization tips, and common mistakes to avoid
        """
        return {
            "recommended_workflow": [
                {
                    "step": 1,
                    "action": "get_manifest",
                    "purpose": "Check if this library can help with your task",
                    "description": "Start by understanding what this library does and its version",
                },
                {
                    "step": 2,
                    "action": "list_modules",
                    "purpose": "Identify relevant modules for your use case",
                    "description": "Browse module structure to find areas that match your needs",
                },
                {
                    "step": 3,
                    "action": "list_symbols",
                    "purpose": "Browse symbols in promising modules",
                    "description": "Use module and kind filters to narrow down to relevant symbols",
                },
                {
                    "step": 4,
                    "action": "get_symbol",
                    "purpose": "Get complete details before implementation",
                    "description": "Always check full signature, required parameters, and return types",
                },
                {
                    "step": 5,
                    "action": "get_class_members",
                    "purpose": "Explore class methods and attributes",
                    "description": "When working with classes, check all available methods",
                },
                {
                    "step": 6,
                    "action": "explore_return_type",
                    "purpose": "Understand what methods are available on returned objects",
                    "description": "Check return type classes to avoid inventing non-existent methods",
                },
            ],
            "cost_optimization": {
                "prefer_browsing": "Use list_modules + list_symbols instead of search_symbols when possible",
                "filter_early": "Always use module and kind parameters in list_symbols to reduce results",
                "validate_before_use": "Always call get_symbol to verify required parameters and return types",
                "check_return_types": "Use explore_return_type or get_class_members on return type classes",
            },
            "common_mistakes": [
                "Starting with search_symbols without first exploring modules (expensive!)",
                "Using symbols without checking required parameters via get_symbol",
                "Assuming return types instead of verifying with get_symbol",
                "Inventing methods on returned objects without checking get_class_members",
                "Not exploring class members with get_class_members before using a class",
            ],
        }

    @mcp.tool()
    def get_manifest() -> dict[str, Any]:
        """Get library metadata including name, version, and compatibility info.

        Use this early to confirm the library matches your needs before exploring further.
        """
        manifest = doc.manifest
        result: dict[str, Any] = {
            "name": manifest.library.name,
            "version": manifest.library.version,
            "language": manifest.library.language,
            "schema_version": manifest.schema_version,
        }
        if manifest.compatibility:
            result["compatibility"] = manifest.compatibility.model_dump(
                exclude_none=True
            )
        return result

    @mcp.tool()
    def list_modules() -> list[str]:
        """Get all unique module paths in the library."""
        return sorted(index.modules)

    @mcp.tool()
    def list_symbols(
        module: str | None = None,
        kind: str | None = None,
    ) -> list[dict[str, Any]]:
        """Browse symbols with optional filtering.

        Args:
            module: Filter by module path (e.g., "json.decoder")
            kind: Filter by symbol kind (function, class, method, attribute, module, constant)

        Returns:
            List of symbol summaries with id, kind, and summary
        """
        # Validate kind if provided
        valid_kinds = [k.value for k in SymbolKind]
        if kind and kind not in valid_kinds:
            return [{"error": f"Invalid kind '{kind}'. Valid options: {valid_kinds}"}]

        # Get candidate symbol IDs
        if module is not None:
            candidates = set(index.symbols_by_module.get(module, []))
        else:
            candidates = set(index.symbols_by_id.keys())

        # Filter by kind if provided
        if kind is not None:
            kind_candidates = set(index.symbols_by_kind.get(kind, []))
            candidates = candidates & kind_candidates

        # Build results
        results = []
        for symbol_id in sorted(candidates):
            symbol = index.symbols_by_id[symbol_id]
            results.append(_symbol_summary(symbol_id, symbol))

        return results

    @mcp.tool()
    def get_symbol(symbol_id: str) -> dict[str, Any]:
        """Get full details for a specific symbol.

        IMPORTANT: Always call this before using a symbol to verify:
        - Required parameters and their types
        - Return type (use explore_return_type for complex types)
        - Whether the function is async

        Args:
            symbol_id: Symbol identifier (e.g., "json:loads", "pathlib:Path#resolve")

        Returns:
            Complete symbol information including signatures, parameters, and semantics
        """
        symbol = index.symbols_by_id.get(symbol_id)
        if symbol is None:
            return {"error": f"Symbol not found: {symbol_id}"}

        result = symbol.model_dump(exclude_none=True)
        result["id"] = symbol_id

        # Add usage hints to help agents use the symbol correctly
        if symbol.signatures:
            sig = symbol.signatures[0]
            required_params = [
                {"name": p.name, "type": p.type}
                for p in (sig.params or [])
                if p.required
            ]
            optional_params = [
                {"name": p.name, "type": p.type, "default": p.default}
                for p in (sig.params or [])
                if not p.required
            ]
            return_type_str = _normalize_return_type(sig.returns)
            result["usage_hints"] = {
                "required_parameters": required_params,
                "optional_parameters": optional_params,
                "is_async": sig.async_ if sig.async_ is not None else False,
                "return_type": return_type_str,
            }
            # Add suggestion to explore return type if it looks like a class
            if return_type_str and not return_type_str.startswith(("str", "int", "float", "bool", "None", "list", "dict", "tuple", "set")):
                result["usage_hints"]["suggestion"] = f"Consider using explore_return_type('{symbol_id}') to see available methods on the returned object"

        return result

    @mcp.tool()
    def search_symbols(
        query: str,
        fields: str | None = None,
    ) -> list[dict[str, Any]]:
        """Find symbols by text search.

        ⚠️  EXPENSIVE OPERATION: This searches ALL symbols and can return large results.

        💡 RECOMMENDED: Try this more efficient workflow first:
           1. list_modules() - find relevant modules
           2. list_symbols(module="...", kind="...") - browse with filters
           3. get_symbol() - get full details

        Only use search_symbols when you need fuzzy text matching across the entire library.

        Args:
            query: Search text (case-insensitive)
            fields: Comma-separated fields to search: name, summary, description (default: all)

        Returns:
            List of matching symbol summaries
        """
        query_lower = query.lower()

        # Parse fields
        if fields:
            search_fields = [f.strip() for f in fields.split(",")]
        else:
            search_fields = ["name", "summary", "description"]

        results = []
        for symbol_id, symbol in index.symbols_by_id.items():
            matched = False

            # Search in name (extracted from symbol_id)
            if "name" in search_fields:
                # Extract name from ID: "module:name" or "module:Class#method"
                name_part = symbol_id.split(":")[-1] if ":" in symbol_id else symbol_id
                if query_lower in name_part.lower():
                    matched = True

            # Search in summary
            if not matched and "summary" in search_fields:
                if query_lower in symbol.semantics.summary.lower():
                    matched = True

            # Search in description
            if not matched and "description" in search_fields:
                if symbol.semantics.description:
                    if query_lower in symbol.semantics.description.lower():
                        matched = True

            if matched:
                results.append(_symbol_summary(symbol_id, symbol))

        return sorted(results, key=lambda x: x["id"])

    @mcp.tool()
    def get_class_members(class_id: str) -> list[dict[str, Any]]:
        """Get all methods and attributes of a class.

        Args:
            class_id: Class identifier (e.g., "pathlib:Path")

        Returns:
            List of member summaries (methods, attributes) belonging to the class
        """
        # Check if the class exists
        if class_id not in index.symbols_by_id:
            return [{"error": f"Class not found: {class_id}"}]

        # Check if it's actually a class
        class_symbol = index.symbols_by_id[class_id]
        if class_symbol.kind != SymbolKind.CLASS:
            return [{"error": f"Symbol '{class_id}' is not a class (kind: {class_symbol.kind.value})"}]

        # Get members
        member_ids = index.class_members.get(class_id, [])
        results = []
        for member_id in sorted(member_ids):
            symbol = index.symbols_by_id[member_id]
            results.append(_symbol_summary(member_id, symbol))

        return results

    @mcp.tool()
    def explore_return_type(symbol_id: str) -> dict[str, Any]:
        """Analyze the return type of a function/method and find related classes.

        Use this to avoid inventing methods on returned objects - check what's actually available.

        Args:
            symbol_id: Function or method identifier (e.g., "module:func", "module:Class#method")

        Returns:
            Return type information and suggested classes to explore with get_class_members
        """
        symbol = index.symbols_by_id.get(symbol_id)
        if symbol is None:
            return {"error": f"Symbol not found: {symbol_id}"}

        if not symbol.signatures:
            return {"error": f"No signature information available for {symbol_id}"}

        sig = symbol.signatures[0]
        return_type_str = _normalize_return_type(sig.returns)
        if not return_type_str:
            return {"message": "No return type information available", "symbol_id": symbol_id}

        result: dict[str, Any] = {
            "symbol_id": symbol_id,
            "return_type": return_type_str,
            "matching_classes": [],
            "suggestions": [],
        }

        # Look for classes that match the return type
        # Handle generic types like List[SomeClass] or Optional[SomeClass]
        type_parts = return_type_str.replace("[", " ").replace("]", " ").replace(",", " ").split()

        for type_part in type_parts:
            # Skip common built-in types
            if type_part.lower() in ("str", "int", "float", "bool", "none", "list", "dict", "tuple", "set", "optional", "any", "union"):
                continue

            # Find matching classes in the index
            for sid, sym in index.symbols_by_id.items():
                if sym.kind == SymbolKind.CLASS:
                    # Match by class name (last part of the ID)
                    class_name = sid.split(":")[-1] if ":" in sid else sid
                    if type_part == class_name or type_part.endswith(class_name):
                        result["matching_classes"].append({
                            "class_id": sid,
                            "summary": sym.semantics.summary,
                        })

        if result["matching_classes"]:
            result["suggestions"].append({
                "action": "get_class_members",
                "targets": [c["class_id"] for c in result["matching_classes"][:3]],
                "reason": f"Explore methods available on {return_type_str} objects",
            })
        else:
            result["suggestions"].append({
                "action": "search_symbols",
                "query": type_parts[0] if type_parts else return_type_str,
                "reason": f"Could not find exact class match for {return_type_str}, try searching",
            })

        return result

    @mcp.tool()
    def get_suggestions(task_description: str) -> dict[str, Any]:
        """Get smart suggestions for exploring this library based on your task.

        Provide a brief description of what you're trying to accomplish,
        and get suggestions for which modules and symbols to explore first.

        Args:
            task_description: Brief description of what you're trying to accomplish

        Returns:
            Suggested modules, symbols, and next exploration steps
        """
        task_lower = task_description.lower()
        task_words = set(task_lower.split())

        suggestions: dict[str, Any] = {
            "task": task_description,
            "suggested_modules": [],
            "suggested_symbols": [],
            "next_steps": [],
        }

        # Find modules with matching names
        for module_name in sorted(index.modules):
            module_lower = module_name.lower()
            # Check if any task word appears in the module name
            if any(word in module_lower for word in task_words if len(word) > 2):
                suggestions["suggested_modules"].append(module_name)

        # Find symbols with matching summaries or names
        for symbol_id, symbol in index.symbols_by_id.items():
            name_part = symbol_id.split(":")[-1] if ":" in symbol_id else symbol_id
            name_lower = name_part.lower()
            summary_lower = symbol.semantics.summary.lower()

            # Check matches in name or summary
            if any(word in name_lower or word in summary_lower for word in task_words if len(word) > 2):
                # Prefer classes and functions over methods
                if symbol.kind in (SymbolKind.CLASS, SymbolKind.FUNCTION):
                    suggestions["suggested_symbols"].append({
                        "id": symbol_id,
                        "kind": symbol.kind.value,
                        "summary": symbol.semantics.summary,
                    })

        # Limit results
        suggestions["suggested_modules"] = suggestions["suggested_modules"][:5]
        suggestions["suggested_symbols"] = suggestions["suggested_symbols"][:10]

        # Generate next steps
        if suggestions["suggested_modules"]:
            for module in suggestions["suggested_modules"][:2]:
                suggestions["next_steps"].append(
                    f"Explore module with: list_symbols(module='{module}')"
                )
        elif suggestions["suggested_symbols"]:
            for sym in suggestions["suggested_symbols"][:2]:
                suggestions["next_steps"].append(
                    f"Get details with: get_symbol('{sym['id']}')"
                )
        else:
            suggestions["next_steps"] = [
                "No direct matches found. Try:",
                "1. list_modules() - browse all available modules",
                "2. list_symbols(kind='class') - see all classes",
                "3. list_symbols(kind='function') - see all functions",
            ]

        return suggestions

    return mcp

run_server

run_server(manifest_path: str | Path, name: str | None = None) -> None

Create and run an MCP server for the given LCP manifest.

Parameters:

Name Type Description Default
manifest_path str | Path

Path to the .lcp.json file

required
name str | None

Server name (default: lcp-{library-name})

None
Source code in src/lcp/mcp_server.py
def run_server(manifest_path: str | Path, name: str | None = None) -> None:
    """Create and run an MCP server for the given LCP manifest.

    Args:
        manifest_path: Path to the .lcp.json file
        name: Server name (default: lcp-{library-name})
    """
    server = create_server(manifest_path, name=name)
    server.run()

create_universal_server

create_universal_server(name: str = 'lcp-universal', cache_dir: Path | str | None = None, no_cache: bool = False, registry_url: str | None = None) -> FastMCP

Create a universal MCP server that resolves any installed Python library.

Unlike create_server (which requires a pre-built manifest), the universal server exposes a resolve_library tool that scans a package on-the-fly and caches the result. All standard exploration tools (list_modules, list_symbols, get_symbol, …) accept an optional library parameter so agents can work with multiple libraries at once.

Parameters:

Name Type Description Default
name str

Server name shown to MCP clients (default: lcp-universal).

'lcp-universal'
cache_dir Path | str | None

Root directory for cached manifests (default: ~/.lcp/cache/).

None
no_cache bool

Disable reading from and writing to the cache.

False
registry_url str | None

Optional base URL of an LCP registry used as a fallback when local scanning fails (e.g. "https://registry.example.com").

None

Returns:

Type Description
FastMCP

Configured FastMCP server instance.

Source code in src/lcp/mcp_server.py
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
def create_universal_server(
    name: str = "lcp-universal",
    cache_dir: Path | str | None = None,
    no_cache: bool = False,
    registry_url: str | None = None,
) -> FastMCP:
    """Create a universal MCP server that resolves any installed Python library.

    Unlike ``create_server`` (which requires a pre-built manifest), the
    universal server exposes a ``resolve_library`` tool that scans a package
    on-the-fly and caches the result.  All standard exploration tools
    (``list_modules``, ``list_symbols``, ``get_symbol``, …) accept an optional
    ``library`` parameter so agents can work with multiple libraries at once.

    Args:
        name: Server name shown to MCP clients (default: ``lcp-universal``).
        cache_dir: Root directory for cached manifests
            (default: ``~/.lcp/cache/``).
        no_cache: Disable reading from and writing to the cache.
        registry_url: Optional base URL of an LCP registry used as a fallback
            when local scanning fails
            (e.g. ``"https://registry.example.com"``).

    Returns:
        Configured FastMCP server instance.
    """
    resolved_cache_dir = Path(cache_dir) if cache_dir else _DEFAULT_CACHE_DIR
    multi_index = MultiLibraryIndex()

    mcp = FastMCP(name)

    # ------------------------------------------------------------------
    # Local helpers
    # ------------------------------------------------------------------

    def _get_index(library: str | None) -> LCPIndex | None:
        return multi_index.get(library)

    def _no_library_error(library: str | None) -> dict[str, Any]:
        if library:
            return {
                "error": f"Library '{library}' is not loaded. "
                f"Call resolve_library('{library}') first."
            }
        return {
            "error": "No library loaded. Call resolve_library(name) first."
        }

    # ------------------------------------------------------------------
    # New tools: resolve_library, list_libraries
    # ------------------------------------------------------------------

    @mcp.tool()
    def resolve_library(name: str) -> dict[str, Any]:
        """Load a Python library's documentation. Call this before using other tools.

        Resolves the library in order:
          1. Local cache  (~/.lcp/cache/{name}/{version}.lcp.json)
          2. Live scan    (pip-installed package)
          3. Registry     (HTTP fetch from the configured registry URL, if set)

        After resolving, this library becomes the implicit default for all
        other tools when no ``library`` parameter is given.

        Args:
            name: Python package name (e.g. "requests", "fastapi").

        Returns:
            Manifest summary with name, version, symbol count, and source.
        """
        try:
            doc, source = resolve_library_document(
                name,
                cache_dir=resolved_cache_dir,
                no_cache=no_cache,
                registry_url=registry_url,
            )
        except ImportError as exc:
            return {"error": str(exc)}

        index = LCPIndex(doc)
        multi_index.add(name, index)

        lib = doc.manifest.library
        return {
            "status": "loaded",
            "name": lib.name,
            "version": lib.version,
            "language": lib.language,
            "symbol_count": len(index.symbols_by_id),
            "module_count": len(index.modules),
            "source": source,
            "next_step": "Use list_modules() or list_symbols() to start exploring.",
        }

    @mcp.tool()
    def list_libraries() -> list[dict[str, Any]]:
        """List all currently loaded libraries.

        Returns:
            Summary of each loaded library (name, version, symbol count).
        """
        return multi_index.list_libraries()

    # ------------------------------------------------------------------
    # Standard exploration tools (library-aware)
    # ------------------------------------------------------------------

    @mcp.tool()
    def get_usage_guide() -> dict[str, Any]:
        """Get strategic guidance on how to efficiently use this universal LCP server.

        CALL THIS FIRST to understand the recommended workflow.

        Returns:
            Recommended workflow, cost optimization tips, and common mistakes to avoid.
        """
        return {
            "recommended_workflow": [
                {
                    "step": 1,
                    "action": "resolve_library",
                    "purpose": "Load a library's documentation",
                    "description": "Call resolve_library('package_name') to scan/load a library",
                },
                {
                    "step": 2,
                    "action": "get_manifest",
                    "purpose": "Check if this library can help with your task",
                    "description": "Confirm library name, version, and language",
                },
                {
                    "step": 3,
                    "action": "list_modules",
                    "purpose": "Identify relevant modules for your use case",
                    "description": "Browse module structure to find areas that match your needs",
                },
                {
                    "step": 4,
                    "action": "list_symbols",
                    "purpose": "Browse symbols in promising modules",
                    "description": "Use module and kind filters to narrow down to relevant symbols",
                },
                {
                    "step": 5,
                    "action": "get_symbol",
                    "purpose": "Get complete details before implementation",
                    "description": "Always check full signature, required parameters, and return types",
                },
                {
                    "step": 6,
                    "action": "get_class_members",
                    "purpose": "Explore class methods and attributes",
                    "description": "When working with classes, check all available methods",
                },
                {
                    "step": 7,
                    "action": "explore_return_type",
                    "purpose": "Understand what methods are available on returned objects",
                    "description": "Check return type classes to avoid inventing non-existent methods",
                },
            ],
            "multi_library_tips": [
                "Call resolve_library('name') for each library you need",
                "Pass library='name' to any tool to target a specific library",
                "The last resolved library is used as the implicit default",
                "Use list_libraries() to see all currently loaded libraries",
            ],
            "cost_optimization": {
                "prefer_browsing": "Use list_modules + list_symbols instead of search_symbols when possible",
                "filter_early": "Always use module and kind parameters in list_symbols to reduce results",
                "validate_before_use": "Always call get_symbol to verify required parameters and return types",
                "check_return_types": "Use explore_return_type or get_class_members on return type classes",
            },
            "common_mistakes": [
                "Forgetting to call resolve_library before using other tools",
                "Starting with search_symbols without first exploring modules (expensive!)",
                "Using symbols without checking required parameters via get_symbol",
                "Assuming return types instead of verifying with get_symbol",
                "Inventing methods on returned objects without checking get_class_members",
            ],
        }

    @mcp.tool()
    def get_manifest(library: str | None = None) -> dict[str, Any]:
        """Get library metadata including name, version, and compatibility info.

        Args:
            library: Library name (default: last resolved library).

        Returns:
            Library metadata dict.
        """
        index = _get_index(library)
        if index is None:
            return _no_library_error(library)

        doc = index.doc
        manifest = doc.manifest
        result: dict[str, Any] = {
            "name": manifest.library.name,
            "version": manifest.library.version,
            "language": manifest.library.language,
            "schema_version": manifest.schema_version,
        }
        if manifest.compatibility:
            result["compatibility"] = manifest.compatibility.model_dump(
                exclude_none=True
            )
        return result

    @mcp.tool()
    def list_modules(library: str | None = None) -> list[str] | dict[str, Any]:
        """Get all unique module paths in the library.

        Args:
            library: Library name (default: last resolved library).

        Returns:
            Sorted list of module paths, or an error dict if library not loaded.
        """
        index = _get_index(library)
        if index is None:
            return _no_library_error(library)
        return sorted(index.modules)

    @mcp.tool()
    def list_symbols(
        module: str | None = None,
        kind: str | None = None,
        library: str | None = None,
    ) -> list[dict[str, Any]]:
        """Browse symbols with optional filtering.

        Args:
            module: Filter by module path (e.g. "json.decoder").
            kind: Filter by symbol kind (function, class, method, attribute, module, constant).
            library: Library name (default: last resolved library).

        Returns:
            List of symbol summaries with id, kind, and summary.
        """
        index = _get_index(library)
        if index is None:
            return [_no_library_error(library)]

        valid_kinds = [k.value for k in SymbolKind]
        if kind and kind not in valid_kinds:
            return [{"error": f"Invalid kind '{kind}'. Valid options: {valid_kinds}"}]

        if module is not None:
            candidates = set(index.symbols_by_module.get(module, []))
        else:
            candidates = set(index.symbols_by_id.keys())

        if kind is not None:
            kind_candidates = set(index.symbols_by_kind.get(kind, []))
            candidates = candidates & kind_candidates

        results = []
        for symbol_id in sorted(candidates):
            symbol = index.symbols_by_id[symbol_id]
            results.append(_symbol_summary(symbol_id, symbol))

        return results

    @mcp.tool()
    def get_symbol(
        symbol_id: str,
        library: str | None = None,
    ) -> dict[str, Any]:
        """Get full details for a specific symbol.

        IMPORTANT: Always call this before using a symbol to verify:
        - Required parameters and their types
        - Return type (use explore_return_type for complex types)
        - Whether the function is async

        Args:
            symbol_id: Symbol identifier (e.g. "json:loads", "pathlib:Path#resolve").
            library: Library name (default: last resolved library).

        Returns:
            Complete symbol information including signatures, parameters, and semantics.
        """
        index = _get_index(library)
        if index is None:
            return _no_library_error(library)

        symbol = index.symbols_by_id.get(symbol_id)
        if symbol is None:
            return {"error": f"Symbol not found: {symbol_id}"}

        result = symbol.model_dump(exclude_none=True)
        result["id"] = symbol_id

        if symbol.signatures:
            sig = symbol.signatures[0]
            required_params = [
                {"name": p.name, "type": p.type}
                for p in (sig.params or [])
                if p.required
            ]
            optional_params = [
                {"name": p.name, "type": p.type, "default": p.default}
                for p in (sig.params or [])
                if not p.required
            ]
            return_type_str = _normalize_return_type(sig.returns)
            result["usage_hints"] = {
                "required_parameters": required_params,
                "optional_parameters": optional_params,
                "is_async": sig.async_ if sig.async_ is not None else False,
                "return_type": return_type_str,
            }
            if return_type_str and not return_type_str.startswith(
                ("str", "int", "float", "bool", "None", "list", "dict", "tuple", "set")
            ):
                result["usage_hints"]["suggestion"] = (
                    f"Consider using explore_return_type('{symbol_id}') "
                    f"to see available methods on the returned object"
                )

        return result

    @mcp.tool()
    def search_symbols(
        query: str,
        fields: str | None = None,
        library: str | None = None,
    ) -> list[dict[str, Any]]:
        """Find symbols by text search.

        ⚠️  EXPENSIVE OPERATION: This searches ALL symbols and can return large results.

        💡 RECOMMENDED: Try this more efficient workflow first:
           1. list_modules() - find relevant modules
           2. list_symbols(module="...", kind="...") - browse with filters
           3. get_symbol() - get full details

        Only use search_symbols when you need fuzzy text matching across the entire library.

        Args:
            query: Search text (case-insensitive).
            fields: Comma-separated fields to search: name, summary, description (default: all).
            library: Library name (default: last resolved library).

        Returns:
            List of matching symbol summaries.
        """
        index = _get_index(library)
        if index is None:
            return [_no_library_error(library)]

        query_lower = query.lower()

        if fields:
            search_fields = [f.strip() for f in fields.split(",")]
        else:
            search_fields = ["name", "summary", "description"]

        results = []
        for symbol_id, symbol in index.symbols_by_id.items():
            matched = False

            if "name" in search_fields:
                name_part = symbol_id.split(":")[-1] if ":" in symbol_id else symbol_id
                if query_lower in name_part.lower():
                    matched = True

            if not matched and "summary" in search_fields:
                if query_lower in symbol.semantics.summary.lower():
                    matched = True

            if not matched and "description" in search_fields:
                if symbol.semantics.description:
                    if query_lower in symbol.semantics.description.lower():
                        matched = True

            if matched:
                results.append(_symbol_summary(symbol_id, symbol))

        return sorted(results, key=lambda x: x["id"])

    @mcp.tool()
    def get_class_members(
        class_id: str,
        library: str | None = None,
    ) -> list[dict[str, Any]]:
        """Get all methods and attributes of a class.

        Args:
            class_id: Class identifier (e.g. "pathlib:Path").
            library: Library name (default: last resolved library).

        Returns:
            List of member summaries (methods, attributes) belonging to the class.
        """
        index = _get_index(library)
        if index is None:
            return [_no_library_error(library)]

        if class_id not in index.symbols_by_id:
            return [{"error": f"Class not found: {class_id}"}]

        class_symbol = index.symbols_by_id[class_id]
        if class_symbol.kind != SymbolKind.CLASS:
            return [
                {
                    "error": f"Symbol '{class_id}' is not a class "
                    f"(kind: {class_symbol.kind.value})"
                }
            ]

        member_ids = index.class_members.get(class_id, [])
        results = []
        for member_id in sorted(member_ids):
            symbol = index.symbols_by_id[member_id]
            results.append(_symbol_summary(member_id, symbol))

        return results

    @mcp.tool()
    def explore_return_type(
        symbol_id: str,
        library: str | None = None,
    ) -> dict[str, Any]:
        """Analyze the return type of a function/method and find related classes.

        Use this to avoid inventing methods on returned objects.

        Args:
            symbol_id: Function or method identifier.
            library: Library name (default: last resolved library).

        Returns:
            Return type information and suggested classes to explore.
        """
        index = _get_index(library)
        if index is None:
            return _no_library_error(library)

        symbol = index.symbols_by_id.get(symbol_id)
        if symbol is None:
            return {"error": f"Symbol not found: {symbol_id}"}

        if not symbol.signatures:
            return {"error": f"No signature information available for {symbol_id}"}

        sig = symbol.signatures[0]
        return_type_str = _normalize_return_type(sig.returns)
        if not return_type_str:
            return {"message": "No return type information available", "symbol_id": symbol_id}

        result: dict[str, Any] = {
            "symbol_id": symbol_id,
            "return_type": return_type_str,
            "matching_classes": [],
            "suggestions": [],
        }

        type_parts = (
            return_type_str.replace("[", " ").replace("]", " ").replace(",", " ").split()
        )

        for type_part in type_parts:
            if type_part.lower() in (
                "str", "int", "float", "bool", "none", "list", "dict",
                "tuple", "set", "optional", "any", "union",
            ):
                continue

            for sid, sym in index.symbols_by_id.items():
                if sym.kind == SymbolKind.CLASS:
                    class_name = sid.split(":")[-1] if ":" in sid else sid
                    if type_part == class_name or type_part.endswith(class_name):
                        result["matching_classes"].append(
                            {"class_id": sid, "summary": sym.semantics.summary}
                        )

        if result["matching_classes"]:
            result["suggestions"].append(
                {
                    "action": "get_class_members",
                    "targets": [c["class_id"] for c in result["matching_classes"][:3]],
                    "reason": f"Explore methods available on {return_type_str} objects",
                }
            )
        else:
            result["suggestions"].append(
                {
                    "action": "search_symbols",
                    "query": type_parts[0] if type_parts else return_type_str,
                    "reason": f"Could not find exact class match for {return_type_str}, try searching",
                }
            )

        return result

    @mcp.tool()
    def get_suggestions(
        task_description: str,
        library: str | None = None,
    ) -> dict[str, Any]:
        """Get smart suggestions for exploring a library based on your task.

        Args:
            task_description: Brief description of what you're trying to accomplish.
            library: Library name (default: last resolved library).

        Returns:
            Suggested modules, symbols, and next exploration steps.
        """
        index = _get_index(library)
        if index is None:
            return _no_library_error(library)

        task_lower = task_description.lower()
        task_words = set(task_lower.split())

        suggestions: dict[str, Any] = {
            "task": task_description,
            "suggested_modules": [],
            "suggested_symbols": [],
            "next_steps": [],
        }

        for module_name in sorted(index.modules):
            module_lower = module_name.lower()
            if any(word in module_lower for word in task_words if len(word) > 2):
                suggestions["suggested_modules"].append(module_name)

        for symbol_id, symbol in index.symbols_by_id.items():
            name_part = symbol_id.split(":")[-1] if ":" in symbol_id else symbol_id
            name_lower = name_part.lower()
            summary_lower = symbol.semantics.summary.lower()

            if any(
                word in name_lower or word in summary_lower
                for word in task_words
                if len(word) > 2
            ):
                if symbol.kind in (SymbolKind.CLASS, SymbolKind.FUNCTION):
                    suggestions["suggested_symbols"].append(
                        {
                            "id": symbol_id,
                            "kind": symbol.kind.value,
                            "summary": symbol.semantics.summary,
                        }
                    )

        suggestions["suggested_modules"] = suggestions["suggested_modules"][:5]
        suggestions["suggested_symbols"] = suggestions["suggested_symbols"][:10]

        if suggestions["suggested_modules"]:
            for module in suggestions["suggested_modules"][:2]:
                suggestions["next_steps"].append(
                    f"Explore module with: list_symbols(module='{module}')"
                )
        elif suggestions["suggested_symbols"]:
            for sym in suggestions["suggested_symbols"][:2]:
                suggestions["next_steps"].append(
                    f"Get details with: get_symbol('{sym['id']}')"
                )
        else:
            suggestions["next_steps"] = [
                "No direct matches found. Try:",
                "1. list_modules() - browse all available modules",
                "2. list_symbols(kind='class') - see all classes",
                "3. list_symbols(kind='function') - see all functions",
            ]

        return suggestions

    return mcp

run_universal_server

run_universal_server(name: str = 'lcp-universal', cache_dir: Path | str | None = None, no_cache: bool = False, registry_url: str | None = None) -> None

Create and run a universal MCP server that resolves any installed Python library.

Parameters:

Name Type Description Default
name str

Server name (default: lcp-universal).

'lcp-universal'
cache_dir Path | str | None

Root directory for cached manifests (default: ~/.lcp/cache/).

None
no_cache bool

Disable reading from and writing to the cache.

False
registry_url str | None

Optional base URL of an LCP registry used as a fallback when local scanning fails.

None
Source code in src/lcp/mcp_server.py
def run_universal_server(
    name: str = "lcp-universal",
    cache_dir: Path | str | None = None,
    no_cache: bool = False,
    registry_url: str | None = None,
) -> None:
    """Create and run a universal MCP server that resolves any installed Python library.

    Args:
        name: Server name (default: lcp-universal).
        cache_dir: Root directory for cached manifests (default: ~/.lcp/cache/).
        no_cache: Disable reading from and writing to the cache.
        registry_url: Optional base URL of an LCP registry used as a fallback
            when local scanning fails.
    """
    server = create_universal_server(
        name=name, cache_dir=cache_dir, no_cache=no_cache, registry_url=registry_url
    )
    server.run()