Repository: Clarifai/clarifai-python
Pull request #592: [EAGLE-6221]: mcp model class
Author: login: zeiler
Created: 2025-05-22T04:10:18Z | Merged: 2025-05-22T18:20:37Z

This pull request introduces several updates, including a new MCP (Model Context Protocol) integration, enhancements to CLI commands, and improved environment variable handling. The changes also include a version bump and updates to testing infrastructure. Below is a categorized summary of the most important changes:

### MCP Integration:
* Added a new `MCPModelClass` in `clarifai/runners/models/mcp_class.py` to handle communication with FastMCP servers, including methods for request/response handling and transport logic.
* Introduced a sample MCP model (`MyModelClass`) and its server implementation in `tests/runners/dummy_mcp_model/1/model.py`, along with a corresponding configuration file (`config.yaml`) and dependencies in `requirements.txt`. [[1]](diffhunk://#diff-8b5b948991c0828b38da7f56eb2aec9363d11f53d772a20a2ff36cf1415e3b62R1-R30) [[2]](diffhunk://#diff-6179bb6680919e949497e17feca6127d2a48f1d97a4a157e18278f089203249aR1-R15) [[3]](diffhunk://#diff-85595419ca40fb1c06bd6be76c28a5d1ebbaaccd2ee80c265c211423d79d18d9R1-R5)

### CLI Enhancements:
* Added a new `env` command to the CLI in `clarifai/cli/base.py` for printing environment variables in a script-friendly format.
* Updated the `local_dev` function in `clarifai/cli/model.py` to log the full model URL and ensure method signatures are fetched without mocking during local development. [[1]](diffhunk://#diff-87a0ccd1cdbf7bbe707ef7b0de9923fd510a6e9342671fc56306193799808040R507-R510) [[2]](diffhunk://#diff-87a0ccd1cdbf7bbe707ef7b0de9923fd510a6e9342671fc56306193799808040L529-R535)

### Configuration and Environment Handling:
* Added a `print_env_vars` method to the configuration utility (`clarifai/utils/config.py`) to output environment variables for easier scripting.

### Model Builder Updates:
* Enhanced the `get_method_signatures` function in `clarifai/runners/models/model_builder.py` to support disabling mocking.
* Added logic to generate an MCP client code snippet when building MCP models. [[1]](diffhunk://#diff-265e698c90224658ed2a2213722fc5ddac5cec98e352802afcbf75760789e383R937-R961) [[2]](diffhunk://#diff-265e698c90224658ed2a2213722fc5ddac5cec98e352802afcbf75760789e383L948-R973)

### Miscellaneous:
* Updated the version in `clarifai/__init__.py` from `11.4.2` to `11.4.3rc1`.

These changes collectively enhance the functionality and flexibility of the Clarifai platform, particularly through the introduction of MCP support and improved developer tooling.


Diff:
diff --git a/clarifai/__init__.py b/clarifai/__init__.py
index be5be47d..57752c2d 100644
--- a/clarifai/__init__.py
+++ b/clarifai/__init__.py
@@ -1 +1 @@
-__version__ = "11.4.2"
+__version__ = "11.4.3rc1"
diff --git a/clarifai/cli/base.py b/clarifai/cli/base.py
index bc462fbc..5ba5b71b 100644
--- a/clarifai/cli/base.py
+++ b/clarifai/cli/base.py
@@ -143,6 +143,13 @@ def dump(ctx_obj, output_format):
         json.dump(ctx_obj.to_dict(), sys.stdout, indent=2)
 
 
+@config.command(['cat'])
+@click.pass_obj
+def env(ctx_obj):
+    """Print env vars. Use: eval "$(clarifai config env)" """
+    ctx_obj.current.print_env_vars()
+
+
 @cli.command()
 @click.argument('api_url', default="https://api.clarifai.com")
 @click.option('--user_id', required=False, help='User ID')
diff --git a/clarifai/cli/model.py b/clarifai/cli/model.py
index 9a2303b1..25140c47 100644
--- a/clarifai/cli/model.py
+++ b/clarifai/cli/model.py
@@ -504,6 +504,10 @@ def local_dev(ctx, model_path):
 
     logger.info(f"Current deployment_id: {deployment_id}")
 
+    logger.info(
+        f"Full url for the model: /users/{user_id}/apps/{app_id}/models/{model.id}/versions/{version.id}"
+    )
+
     # Now that we have all the context in ctx.obj, we need to update the config.yaml in
     # the model_path directory with the model object containing user_id, app_id, model_id, version_id
     config_file = os.path.join(model_path, 'config.yaml')
@@ -526,13 +530,9 @@ def local_dev(ctx, model_path):
         ModelBuilder._backup_config(config_file)
         ModelBuilder._save_config(config_file, config)
 
-    # client_model = Model(
-    # TODO: once we can generate_client_script from ModelBuilder or similar
-    # we should be able to put the exact function call in place.
-    # model_script = model.generate_client_script()
-
     builder = ModelBuilder(model_path, download_validation_only=True)
-    method_signatures = builder.get_method_signatures()
+    # don't mock for local dev since you need the dependencies to run the code anyways.
+    method_signatures = builder.get_method_signatures(mocking=False)
 
     from clarifai.runners.utils import code_script
 
@@ -546,8 +546,6 @@ def local_dev(ctx, model_path):
         base_url=ctx.obj.current.api_base,
     )
 
-    # TODO: put in the ClarifaiUrlHelper to create the model url.
-
     logger.info("""\n
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
 # About to start up the local dev runner in this terminal...
diff --git a/clarifai/client/app.py b/clarifai/client/app.py
index 5bd271a9..c1f085b4 100644
--- a/clarifai/client/app.py
+++ b/clarifai/client/app.py
@@ -19,6 +19,7 @@
 from clarifai.constants.model import TRAINABLE_MODEL_TYPES
 from clarifai.errors import UserError
 from clarifai.urls.helper import ClarifaiUrlHelper
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import display_concept_relations_tree, display_workflow_tree, logger
 from clarifai.utils.misc import concept_relations_accumulation
 from clarifai.workflows.utils import get_yaml_output_info_proto, is_same_yaml_model
@@ -33,7 +34,7 @@ def __init__(
         url: str = None,
         app_id: str = None,
         user_id: str = None,
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/auth/helper.py b/clarifai/client/auth/helper.py
index e448105b..20105dc6 100644
--- a/clarifai/client/auth/helper.py
+++ b/clarifai/client/auth/helper.py
@@ -7,10 +7,12 @@
 from clarifai_grpc.grpc.api import resources_pb2, service_pb2_grpc
 
 from clarifai import __version__
-from clarifai.utils.constants import CLARIFAI_PAT_ENV_VAR, CLARIFAI_SESSION_TOKEN_ENV_VAR
-
-DEFAULT_BASE = "https://api.clarifai.com"
-DEFAULT_UI = "https://clarifai.com"
+from clarifai.utils.constants import (
+    CLARIFAI_PAT_ENV_VAR,
+    CLARIFAI_SESSION_TOKEN_ENV_VAR,
+    DEFAULT_BASE,
+    DEFAULT_UI,
+)
 
 REQUEST_ID_PREFIX_HEADER = "x-clarifai-request-id-prefix"
 REQUEST_ID_PREFIX = f"sdk-python-{__version__}"
diff --git a/clarifai/client/compute_cluster.py b/clarifai/client/compute_cluster.py
index 00f80d1b..a75923fb 100644
--- a/clarifai/client/compute_cluster.py
+++ b/clarifai/client/compute_cluster.py
@@ -10,6 +10,7 @@
 from clarifai.client.lister import Lister
 from clarifai.client.nodepool import Nodepool
 from clarifai.errors import UserError
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 
 
@@ -20,7 +21,7 @@ def __init__(
         self,
         compute_cluster_id: str = None,
         user_id: str = None,
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/dataset.py b/clarifai/client/dataset.py
index 7254f418..0eaab934 100644
--- a/clarifai/client/dataset.py
+++ b/clarifai/client/dataset.py
@@ -45,6 +45,7 @@
     VisualSegmentationDataset,
     TextClassificationDataset,
 )
+from clarifai.utils.constants import DEFAULT_BASE
 
 
 class Dataset(Lister, BaseClient):
@@ -55,7 +56,7 @@ def __init__(
         url: str = None,
         dataset_id: str = None,
         dataset_version_id: str = None,
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/deployment.py b/clarifai/client/deployment.py
index c2290dd6..39895aa8 100644
--- a/clarifai/client/deployment.py
+++ b/clarifai/client/deployment.py
@@ -2,6 +2,7 @@
 
 from clarifai.client.base import BaseClient
 from clarifai.client.lister import Lister
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 from clarifai.utils.protobuf import dict_to_protobuf
 
@@ -13,7 +14,7 @@ def __init__(
         self,
         deployment_id: str = None,
         user_id: str = None,
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/input.py b/clarifai/client/input.py
index 627eeeb7..e21fafff 100644
--- a/clarifai/client/input.py
+++ b/clarifai/client/input.py
@@ -21,6 +21,7 @@
 from clarifai.constants.dataset import MAX_RETRIES
 from clarifai.constants.input import MAX_UPLOAD_BATCH_SIZE
 from clarifai.errors import UserError
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 from clarifai.utils.misc import BackoffIterator, Chunker, clean_input_id
 
@@ -33,7 +34,7 @@ def __init__(
         user_id: str = None,
         app_id: str = None,
         logger_level: str = "INFO",
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/model.py b/clarifai/client/model.py
index b5f0962a..f22f444b 100644
--- a/clarifai/client/model.py
+++ b/clarifai/client/model.py
@@ -34,6 +34,7 @@
 )
 from clarifai.errors import UserError
 from clarifai.urls.helper import ClarifaiUrlHelper
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 from clarifai.utils.misc import BackoffIterator
 from clarifai.utils.model_train import (
@@ -58,7 +59,7 @@ def __init__(
         url: str = None,
         model_id: str = None,
         model_version: Dict = {'id': ""},
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/module.py b/clarifai/client/module.py
index 0351f382..4e2619a4 100644
--- a/clarifai/client/module.py
+++ b/clarifai/client/module.py
@@ -6,6 +6,7 @@
 from clarifai.client.lister import Lister
 from clarifai.errors import UserError
 from clarifai.urls.helper import ClarifaiUrlHelper
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 
 
@@ -17,7 +18,7 @@ def __init__(
         url: str = None,
         module_id: str = None,
         module_version: Dict = {'id': ""},
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/nodepool.py b/clarifai/client/nodepool.py
index 9be42e9a..d76486c7 100644
--- a/clarifai/client/nodepool.py
+++ b/clarifai/client/nodepool.py
@@ -11,6 +11,7 @@
 from clarifai.client.lister import Lister
 from clarifai.client.runner import Runner
 from clarifai.errors import UserError
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 
 
@@ -21,7 +22,7 @@ def __init__(
         self,
         nodepool_id: str = None,
         user_id: str = None,
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/runner.py b/clarifai/client/runner.py
index 1d1e1474..170095e0 100644
--- a/clarifai/client/runner.py
+++ b/clarifai/client/runner.py
@@ -2,6 +2,7 @@
 
 from clarifai.client.base import BaseClient
 from clarifai.client.lister import Lister
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 from clarifai.utils.protobuf import dict_to_protobuf
 
@@ -13,7 +14,7 @@ def __init__(
         self,
         runner_id: str = None,
         user_id: str = None,
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/search.py b/clarifai/client/search.py
index 03e408b4..d48c9f57 100644
--- a/clarifai/client/search.py
+++ b/clarifai/client/search.py
@@ -17,6 +17,7 @@
 )
 from clarifai.errors import UserError
 from clarifai.schema.search import get_schema
+from clarifai.utils.constants import DEFAULT_BASE
 
 
 class Search(Lister, BaseClient):
@@ -28,7 +29,7 @@ def __init__(
         metric: str = DEFAULT_SEARCH_METRIC,
         algorithm: str = DEFAULT_SEARCH_ALGORITHM,
         pagination: bool = False,
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/user.py b/clarifai/client/user.py
index 18b8235c..23df329b 100644
--- a/clarifai/client/user.py
+++ b/clarifai/client/user.py
@@ -12,6 +12,7 @@
 from clarifai.client.compute_cluster import ComputeCluster
 from clarifai.client.lister import Lister
 from clarifai.errors import UserError
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 
 
@@ -21,7 +22,7 @@ class User(Lister, BaseClient):
     def __init__(
         self,
         user_id: str = None,
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/client/workflow.py b/clarifai/client/workflow.py
index bcaa51c2..d2f85f70 100644
--- a/clarifai/client/workflow.py
+++ b/clarifai/client/workflow.py
@@ -14,6 +14,7 @@
 from clarifai.constants.workflow import MAX_WORKFLOW_PREDICT_INPUTS
 from clarifai.errors import UserError
 from clarifai.urls.helper import ClarifaiUrlHelper
+from clarifai.utils.constants import DEFAULT_BASE
 from clarifai.utils.logging import logger
 from clarifai.utils.misc import BackoffIterator, status_is_retryable
 from clarifai.workflows.export import Exporter
@@ -28,7 +29,7 @@ def __init__(
         workflow_id: str = None,
         workflow_version: Dict = {'id': ""},
         output_config: Dict = {'min_value': 0},
-        base_url: str = "https://api.clarifai.com",
+        base_url: str = DEFAULT_BASE,
         pat: str = None,
         token: str = None,
         root_certificates_path: str = None,
diff --git a/clarifai/runners/models/mcp_class.py b/clarifai/runners/models/mcp_class.py
new file mode 100644
index 00000000..e13a9779
--- /dev/null
+++ b/clarifai/runners/models/mcp_class.py
@@ -0,0 +1,114 @@
+"""Base class for creating Model Context Protocol (MCP) servers."""
+
+import asyncio
+import json
+from typing import Any
+
+from fastmcp import Client, FastMCP  # use fastmcp v2 not the built in mcp
+from mcp import types
+from mcp.shared.exceptions import McpError
+
+from clarifai.runners.models.model_class import ModelClass
+
+
+class MCPModelClass(ModelClass):
+    """Base class for wrapping FastMCP servers as a model running in Clarfai. This handles
+    all the transport between the API and the MCP server here. Simply subclass this and implement
+    the get_server() method to return the FastMCP server instance. The server is then used to
+    handle all the requests and responses.
+    """
+
+    def load_model(self):
+        # in memory transport provided in fastmcp v2 so we can easily use the client functions.
+        self.client = Client(self.get_server())
+
+    def get_server(self) -> FastMCP:
+        """Required method for each subclass to implement to return the FastMCP server to use."""
+        raise NotImplementedError("Subclasses must implement get_server() method")
+
+    @ModelClass.method
+    def mcp_transport(self, msg: str) -> str:
+        """The single model method to get the jsonrpc message and send it to the FastMCP server then
+        return it's response.
+
+        """
+
+        async def send_notification(client_message: types.ClientNotification) -> None:
+            async with self.client:
+                # Strip the jsonrpc field since send_notification will also pass it in for some reason.
+                client_message = types.ClientNotification.model_validate(
+                    client_message.model_dump(
+                        by_alias=True, mode="json", exclude_none=True, exclude={"jsonrpc"}
+                    )
+                )
+                try:
+                    return await self.client.session.send_notification(client_message)
+                except McpError as e:
+                    return types.JSONRPCError(jsonrpc="2.0", error=e.error)
+
+        async def send_request(client_message: types.ClientRequest, id: str) -> Any:
+            async with self.client:
+                # Strip the jsonrpc and id fields as send_request sets them again too.
+                client_message = types.ClientRequest.model_validate(
+                    client_message.model_dump(
+                        by_alias=True, mode="json", exclude_none=True, exclude={"jsonrpc", "id"}
+                    )
+                )
+
+                result_type = None
+                if isinstance(client_message.root, types.PingRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.InitializeRequest):
+                    return await self.client.session.initialize()
+                elif isinstance(client_message.root, types.SetLevelRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.ListResourcesRequest):
+                    result_type = types.ListResourcesResult
+                elif isinstance(client_message.root, types.ListResourceTemplatesRequest):
+                    result_type = types.ListResourceTemplatesResult
+                elif isinstance(client_message.root, types.ReadResourceRequest):
+                    result_type = types.ReadResourceResult
+                elif isinstance(client_message.root, types.SubscribeRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.UnsubscribeRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.ListPromptsRequest):
+                    result_type = types.ListPromptsResult
+                elif isinstance(client_message.root, types.GetPromptRequest):
+                    result_type = types.GetPromptResult
+                elif isinstance(client_message.root, types.CompleteRequest):
+                    result_type = types.CompleteResult
+                elif isinstance(client_message.root, types.ListToolsRequest):
+                    result_type = types.ListToolsResult
+                elif isinstance(client_message.root, types.CallToolRequest):
+                    result_type = types.CallToolResult
+                else:
+                    # this is a special case where we need to return the list of tools.
+                    raise NotImplementedError(f"Method {client_message.method} not implemented")
+                # Call the mcp server using send_request() or send_notification() depending on the method.
+                try:
+                    return await self.client.session.send_request(client_message, result_type)
+                except McpError as e:
+                    return types.JSONRPCError(jsonrpc="2.0", id=id, error=e.error)
+
+        # The message coming here is the generic request. We look at it's .method
+        # to determine which client function to call and to further subparse the params.
+        # Note(zeiler): unfortunately the pydantic types in mcp/types.py are not consistent.
+        # The JSONRPCRequest are supposed to have an id but the InitializeRequest
+        # does not have it.
+        d = json.loads(msg)
+        id = d.get('id', "")
+
+        # If we have an id it's a JSONRPCRequest
+        if not d.get('method', '').startswith("notifications/"):
+            client_message = types.ClientRequest.model_validate(d)
+            response = asyncio.run(send_request(client_message, id=id))
+        else:  # JSONRPCRequest
+            client_message = types.ClientNotification.model_validate(d)
+            response = asyncio.run(send_notification(client_message))
+        if response is None:
+            response = types.JSONRPCError(
+                jsonrpc="2.0", id=id, error="Got empty response from MCP server."
+            )
+        # return as a serialized json string
+        return response.model_dump_json(by_alias=True, exclude_none=True)
diff --git a/clarifai/runners/models/model_builder.py b/clarifai/runners/models/model_builder.py
index 19194b39..8cc3f25c 100644
--- a/clarifai/runners/models/model_builder.py
+++ b/clarifai/runners/models/model_builder.py
@@ -410,11 +410,17 @@ def method_signatures_yaml(self):
         signatures = {method.name: method.signature for method in method_info.values()}
         return signatures_to_yaml(signatures)
 
-    def get_method_signatures(self):
+    def get_method_signatures(self, mocking=True):
         """
         Returns the method signatures for the model class.
+
+        Args:
+          mocking (bool): Whether to mock the model class or not. Defaults to False.
+
+        Returns:
+          list: A list of method signatures for the model class.
         """
-        model_class = self.load_model_class(mocking=True)
+        model_class = self.load_model_class(mocking=mocking)
         method_info = model_class._get_method_info()
         signatures = [method.signature for method in method_info.values()]
         return signatures
@@ -436,22 +442,42 @@ def client(self):
         return self._client
 
     @property
-    def model_url(self):
+    def model_ui_url(self):
+        url_helper = ClarifaiUrlHelper(self._client.auth_helper)
+        # Note(zeiler): the UI experience isn't the best when including version id right now.
+        # if self.model_version_id is not None:
+        #     return url_helper.clarifai_url(
+        #         self.client.user_app_id.user_id,
+        #         self.client.user_app_id.app_id,
+        #         "models",
+        #         self.model_id,
+        #         self.model_version_id,
+        #     )
+        # else:
+        return url_helper.clarifai_url(
+            self.client.user_app_id.user_id,
+            self.client.user_app_id.app_id,
+            "models",
+            self.model_id,
+        )
+
+    @property
+    def model_api_url(self):
         url_helper = ClarifaiUrlHelper(self._client.auth_helper)
         if self.model_version_id is not None:
-            return url_helper.clarifai_url(
+            return url_helper.api_url(
                 self.client.user_app_id.user_id,
                 self.client.user_app_id.app_id,
                 "models",
                 self.model_id,
+                self.model_version_id,
             )
         else:
-            return url_helper.clarifai_url(
+            return url_helper.api_url(
                 self.client.user_app_id.user_id,
                 self.client.user_app_id.app_id,
                 "models",
                 self.model_id,
-                self.model_version_id,
             )
 
     def _get_model_proto(self):
@@ -930,10 +956,11 @@ def filter_func(tarinfo):
             return
         self.model_version_id = response.model_version_id
         logger.info(f"Created Model Version ID: {self.model_version_id}")
-        logger.info(f"Full url to that version is: {self.model_url}")
+        logger.info(f"Full url to that version is: {self.model_ui_url}")
         try:
             is_uploaded = self.monitor_model_build()
             if is_uploaded:
+                # python code to run the model.
                 from clarifai.runners.utils import code_script
 
                 method_signatures = self.get_method_signatures()
@@ -945,7 +972,7 @@ def filter_func(tarinfo):
                 )
                 logger.info("""\n
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-# Here is a code snippet to call this model:
+# Here is a code snippet to use this model:
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                 """)
                 logger.info(snippet)
@@ -1049,7 +1076,7 @@ def monitor_model_build(self):
                 logger.info("Model build complete!")
                 logger.info(f"Build time elapsed {time.time() - st:.1f}s)")
                 logger.info(
-                    f"Check out the model at {self.model_url} version: {self.model_version_id}"
+                    f"Check out the model at {self.model_ui_url} version: {self.model_version_id}"
                 )
                 return True
             else:
@@ -1074,10 +1101,12 @@ def upload_model(folder, stage, skip_dockerfile):
     exists = builder.check_model_exists()
     if exists:
         logger.info(
-            f"Model already exists at {builder.model_url}, this upload will create a new version for it."
+            f"Model already exists at {builder.model_ui_url}, this upload will create a new version for it."
         )
     else:
-        logger.info(f"New model will be created at {builder.model_url} with it's first version.")
+        logger.info(
+            f"New model will be created at {builder.model_ui_url} with it's first version."
+        )
 
     input("Press Enter to continue...")
     builder.upload_model_version()
diff --git a/clarifai/runners/utils/code_script.py b/clarifai/runners/utils/code_script.py
index dbeab1b4..3d26946f 100644
--- a/clarifai/runners/utils/code_script.py
+++ b/clarifai/runners/utils/code_script.py
@@ -4,6 +4,7 @@
 from clarifai_grpc.grpc.api import resources_pb2
 
 from clarifai.runners.utils import data_utils
+from clarifai.urls.helper import ClarifaiUrlHelper
 
 
 def generate_client_script(
@@ -15,6 +16,38 @@ def generate_client_script(
     deployment_id: str = None,
     use_ctx: bool = False,
 ) -> str:
+    url_helper = ClarifaiUrlHelper()
+
+    # Provide an mcp client config
+    if len(method_signatures) == 1 and method_signatures[0].name == "mcp_transport":
+        api_url = url_helper.api_url(
+            user_id,
+            app_id,
+            "models",
+            model_id,
+        )
+
+        _CLIENT_TEMPLATE = """
+import asyncio
+import os
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+
+transport = StreamableHttpTransport(url="%s/mcp",
+                                    headers={"Authorization": "Bearer " + os.environ["CLARIFAI_PAT"]})
+
+async def main():
+  async with Client(transport) as client:
+    tools = await client.list_tools()
+    print(f"Available tools: {tools}")
+    result = await client.call_tool(tools[0].name, {"a": 5, "b": 3})
+    print(f"Result: {result[0].text}")
+
+if __name__ == "__main__":
+  asyncio.run(main())
+"""
+        return _CLIENT_TEMPLATE % api_url
+
     _CLIENT_TEMPLATE = """\
 import os
 
@@ -35,8 +68,9 @@ def generate_client_script(
         model_section = """
 model = Model.from_current_context()"""
     else:
+        model_ui_url = url_helper.clarifai_url(user_id, app_id, "models", model_id)
         model_section = f"""
-model = Model("https://clarifai.com/{user_id}/{app_id}/models/{model_id}",
+model = Model({model_ui_url},
                deployment_id = {deployment_id}, # Only needed for dedicated deployed models
                {base_url_str}
  )
diff --git a/clarifai/urls/helper.py b/clarifai/urls/helper.py
index e53b7b99..7ebd0b3b 100644
--- a/clarifai/urls/helper.py
+++ b/clarifai/urls/helper.py
@@ -1,10 +1,25 @@
+import os
+from collections import namedtuple
 from urllib.parse import urlparse
 
+from clarifai.utils.constants import DEFAULT_BASE, DEFAULT_UI
+
+# To help with using ClarifaiUrlHelper with defaults as ClarifaiUrlHelper()
+auth_obj = namedtuple("auth", ["ui", "base"])
+default_auth = auth_obj(
+    ui=os.environ.get("CLARIFAI_UI", DEFAULT_UI),
+    base=os.environ.get("CLARIFAI_API_BASE", DEFAULT_BASE),
+)
+
 
 class ClarifaiUrlHelper(object):
     """Lots of helper functionality for dealing with urls around modules."""
 
-    def __init__(self, auth, module_manager_imv_id="module_manager_install"):
+    def __init__(
+        self,
+        auth=default_auth,
+        module_manager_imv_id: str = "module_manager_install",
+    ):
         """
         Args:
           auth: a ClarifaiAuthHelper object.
@@ -13,13 +28,17 @@ def __init__(self, auth, module_manager_imv_id="module_manager_install"):
         self._module_manager_imv_id = module_manager_imv_id
 
     @property
-    def auth(self):
-        return self._auth
+    def ui(self):
+        return self._auth.ui
+
+    @property
+    def base(self):
+        return self._auth.base
 
     def module_ui_url(self, user_id, app_id, module_id, module_version_id):
         """This is the path to the module in community."""
         return "%s/%s/%s/modules/%s/versions/%s" % (
-            self.auth.ui,
+            self.ui,
             user_id,
             app_id,
             module_id,
@@ -30,7 +49,7 @@ def module_install_ui_url(self, dest_user_id, dest_app_id, module_url):
         """This is a url that allows for installation of the module from the community at 'module_url'
         into the destination app_id of the destination user_id."""
         return "%s/%s/%s/installed_module_versions/%s/install?install=%s" % (
-            self.auth.ui,
+            self.ui,
             dest_user_id,
             dest_app_id,
             self._module_manager_imv_id,
@@ -39,22 +58,51 @@ def module_install_ui_url(self, dest_user_id, dest_app_id, module_url):
 
     def imv_ui_url(self, dest_user_id, dest_app_id, imv_id):
         return "%s/%s/%s/installed_module_versions/%s" % (
-            self.auth.ui,
+            self.ui,
             dest_user_id,
             dest_app_id,
             imv_id,
         )
 
-    def clarifai_url(self, user_id, app_id, resource_type, resource_id, version_id: str = None):
-        """This is the path to the resource in community.
+    def api_url(self, user_id, app_id, resource_type, resource_id, version_id: str = None):
+        """This is the path to the resource in the API.
+
+        Example:
+          https://api.clarifai.com/v2/zeiler/app/modules/module1/versions/2
+          https://api.clarifai.com/v2/zeiler/app/models/model1/versions/2
+          https://api.clarifai.com/v2/zeiler/app/concepts/concept1
+          https://api.clarifai.com/v2/zeiler/app/workflows/workflow1
+          https://api.clarifai.com/v2/zeiler/app/tasks/task1
+          https://api.clarifai.com/v2/zeiler/app/installed_module_versions/module_manager_install
 
         Args:
           user_id: the author of the resource.
           app_id: the author's app the resource was created in.
-          resource_type: the type of resource. One of "modules", "models", "concepts", "inputs", "workflows", "tasks", "installed_module_versions"
+          resource_type: the type of resource. One of "modules", "models", "concepts", "inputs", "workflows", "tasks"
           resource_id: the resource ID
-          version_id: the version of the resource.
         """
+        self._validate_resource_type(resource_type)
+        if version_id is None:
+            return "%s/v2/users/%s/apps/%s/%s/%s" % (
+                self.base,
+                user_id,
+                app_id,
+                resource_type,
+                resource_id,
+            )
+
+        if resource_type in ["concepts", "tasks", "installed_module_versions"]:
+            raise ValueError(f"{resource_type} do not have versions.")
+        return "%s/v2/users/%s/apps/%s/%s/%s/versions/%s" % (
+            self.base,
+            user_id,
+            app_id,
+            resource_type,
+            resource_id,
+            version_id,
+        )
+
+    def _validate_resource_type(self, resource_type):
         if resource_type not in [
             "modules",
             "models",
@@ -68,10 +116,30 @@ def clarifai_url(self, user_id, app_id, resource_type, resource_id, version_id:
                 "resource_type must be one of modules, models, concepts, inputs, workflows, tasks, installed_module_versions but was %s"
                 % resource_type
             )
+
+    def clarifai_url(self, user_id, app_id, resource_type, resource_id, version_id: str = None):
+        """This is the path to the resource in community UI.
+
+        Example:
+          https://clarifai.com/zeiler/modules/module1/versions/2
+          https://clarifai.com/zeiler/models/model1/versions/2
+          https://clarifai.com/zeiler/concepts/concept1
+          https://clarifai.com/zeiler/workflows/workflow1
+          https://clarifai.com/zeiler/tasks/task1
+          https://clarifai.com/zeiler/installed_module_versions/module_manager_install
+
+        Args:
+          user_id: the author of the resource.
+          app_id: the author's app the resource was created in.
+          resource_type: the type of resource. One of "modules", "models", "concepts", "inputs", "workflows", "tasks", "installed_module_versions"
+          resource_id: the resource ID
+          version_id: the version of the resource.
+        """
+        self._validate_resource_type(resource_type)
         if version_id is None:
-            return "%s/%s/%s/%s/%s" % (self.auth.ui, user_id, app_id, resource_type, resource_id)
+            return "%s/%s/%s/%s/%s" % (self.ui, user_id, app_id, resource_type, resource_id)
         return "%s/%s/%s/%s/%s/versions/%s" % (
-            self.auth.ui,
+            self.ui,
             user_id,
             app_id,
             resource_type,
diff --git a/clarifai/utils/config.py b/clarifai/utils/config.py
index a04545cf..2f9dcb72 100644
--- a/clarifai/utils/config.py
+++ b/clarifai/utils/config.py
@@ -102,6 +102,25 @@ def set_to_env(self):
                 envvar_name = 'CLARIFAI_' + envvar_name
             os.environ[envvar_name] = str(v)
 
+    def print_env_vars(self):
+        """prints the context env vars to the terminal
+
+        Example:
+          # This is helpful in scripts so you can do
+
+          from clarifai.utils.config import Config
+
+          Config.from_yaml().current.print_env_vars()
+
+        """
+        for k, v in sorted(self['env'].items()):
+            if isinstance(v, dict):
+                continue
+            envvar_name = k.upper()
+            if not envvar_name.startswith('CLARIFAI_'):
+                envvar_name = 'CLARIFAI_' + envvar_name
+            print(f"export {envvar_name}=\"{v}\"")
+
 
 @dataclass
 class Config:
diff --git a/clarifai/utils/constants.py b/clarifai/utils/constants.py
index fcb5cb17..dfb50b1d 100644
--- a/clarifai/utils/constants.py
+++ b/clarifai/utils/constants.py
@@ -1,5 +1,9 @@
 import os
 
+DEFAULT_UI = os.environ.get("CLARIFAI_UI", "https://clarifai.com")
+DEFAULT_BASE = os.environ.get("CLARIFAI_API_BASE", "https://api.clarifai.com")
+
+
 CLARIFAI_PAT_ENV_VAR = "CLARIFAI_PAT"
 CLARIFAI_SESSION_TOKEN_ENV_VAR = "CLARIFAI_SESSION_TOKEN"
 CLARIFAI_USER_ID_ENV_VAR = "CLARIFAI_USER_ID"
diff --git a/tests/runners/dummy_mcp_model/1/model.py b/tests/runners/dummy_mcp_model/1/model.py
new file mode 100644
index 00000000..a4ffbf36
--- /dev/null
+++ b/tests/runners/dummy_mcp_model/1/model.py
@@ -0,0 +1,30 @@
+from typing import Any
+
+from fastmcp import FastMCP  # use fastmcp v2 not the built in mcp
+from pydantic import Field
+
+from clarifai.runners.models.mcp_class import MCPModelClass
+
+server = FastMCP("my-first-mcp-server", instructions="", stateless_http=True)
+
+
+@server.tool("calculate_sum", description="Add two numbers together")
+def sum(a: Any = Field(description="first number"), b: Any = Field(description="second number")):
+    return float(a) + float(b)
+
+
+# Static resource
+@server.resource("config://version")
+def get_version():
+    return "2.0.1"
+
+
+@server.prompt()
+def summarize_request(text: str) -> str:
+    """Generate a prompt asking for a summary."""
+    return f"Please summarize the following text:\n\n{text}"
+
+
+class MyModelClass(MCPModelClass):
+    def get_server(self) -> FastMCP:
+        return server
diff --git a/tests/runners/dummy_mcp_model/config.yaml b/tests/runners/dummy_mcp_model/config.yaml
new file mode 100644
index 00000000..a0ae0bd9
--- /dev/null
+++ b/tests/runners/dummy_mcp_model/config.yaml
@@ -0,0 +1,15 @@
+# This is the sample config file for a MCP model.
+
+model:
+  id: "dummy-runner-model"
+  user_id: "user_id"
+  app_id: "app_id"
+  model_type_id: "mcp"
+
+build_info:
+  python_version: "3.12"
+
+inference_compute_info:
+  cpu_limit: "1"
+  cpu_memory: "1Gi"
+  num_accelerators: 0
diff --git a/tests/runners/dummy_mcp_model/requirements.txt b/tests/runners/dummy_mcp_model/requirements.txt
new file mode 100644
index 00000000..91646b8a
--- /dev/null
+++ b/tests/runners/dummy_mcp_model/requirements.txt
@@ -0,0 +1,5 @@
+aiohttp
+requests
+clarifai
+huggingface_hub
+fastmcp>=2.3.4
diff --git a/tests/test_modules.py b/tests/test_modules.py
index 1bd7f214..8020f69b 100644
--- a/tests/test_modules.py
+++ b/tests/test_modules.py
@@ -1,40 +1,34 @@
-from collections import namedtuple
-
 import pytest
 
 from clarifai.urls.helper import ClarifaiUrlHelper
-
-auth_obj = namedtuple("auth", "ui")
+from clarifai.utils.constants import DEFAULT_UI
 
 
 @pytest.fixture()
 def helper():
-    auth = auth_obj(ui="http://fake")
-    return ClarifaiUrlHelper(auth)
+    return ClarifaiUrlHelper()
 
 
 def test_module_Ui_url(helper):
     url = helper.module_ui_url("clarifai", "main", "module_1", "version_1")
-    assert url == "http://fake/clarifai/main/modules/module_1/versions/version_1"
+    assert url == f"{DEFAULT_UI}/clarifai/main/modules/module_1/versions/version_1"
 
 
 def test_module_install_url(helper):
     install_url = helper.module_install_ui_url("person", "app", "XXX")
     assert (
         install_url
-        == "http://fake/person/app/installed_module_versions/module_manager_install/install?install=XXX"
+        == f"{DEFAULT_UI}/person/app/installed_module_versions/module_manager_install/install?install=XXX"
     )
 
 
 def test_install_with_custom_imv_id():
-    auth = auth_obj(ui="http://fake")
-
     custom_imv_id = "some_imv_id"
-    helper = ClarifaiUrlHelper(auth, custom_imv_id)
+    helper = ClarifaiUrlHelper(module_manager_imv_id=custom_imv_id)
     install_url = helper.module_install_ui_url("person", "app", "XXX")
     assert (
         install_url
-        == "http://fake/person/app/installed_module_versions/some_imv_id/install?install=XXX"
+        == f"{DEFAULT_UI}/person/app/installed_module_versions/some_imv_id/install?install=XXX"
     )
 
 
@@ -109,7 +103,6 @@ def test_no_right_model_version_in_path():
 
 
 def test_split_model_with_https_prefix():
-    auth = auth_obj(ui="https://clarifai.com")
     url = "https://clarifai.com/clarifai/main/models/model_1/versions/2"
     (
         user_id,
@@ -124,14 +117,13 @@ def test_split_model_with_https_prefix():
     assert model_id == "model_1"
     assert model_version_id == "2"
 
-    new = ClarifaiUrlHelper(auth).clarifai_url(
+    new = ClarifaiUrlHelper().clarifai_url(
         user_id, app_id, resource_type, model_id, model_version_id
     )
     assert new == url
 
 
 def test_split_model_without_version_with_https_prefix():
-    auth = auth_obj(ui="https://clarifai.com")
     url = "https://clarifai.com/clarifai/main/models/model_1"
     (
         user_id,
@@ -146,15 +138,14 @@ def test_split_model_without_version_with_https_prefix():
     assert model_id == "model_1"
     assert model_version_id is None
 
-    new = ClarifaiUrlHelper(auth).clarifai_url(
+    new = ClarifaiUrlHelper().clarifai_url(
         user_id, app_id, resource_type, model_id, model_version_id
     )
     assert new == url
 
 
 def test_bad_resource_type_in_clarifai_url():
-    auth = auth_obj(ui="https://clarifai.com")
-    helper = ClarifaiUrlHelper(auth)
+    helper = ClarifaiUrlHelper()
 
     with pytest.raises(
         ValueError,
diff --git a/tests/test_rag.py b/tests/test_rag.py
index eee0ec20..f87adb94 100644
--- a/tests/test_rag.py
+++ b/tests/test_rag.py
@@ -28,8 +28,7 @@ class TestRAG:
     def setup_class(self):
         self.rag = RAG.setup(user_id=CREATE_APP_USER_ID, base_url=CLARIFAI_API_BASE)
         wf = self.rag._prompt_workflow
-        auth = auth_obj(ui="https://clarifai.com")
-        self.workflow_url = ClarifaiUrlHelper(auth).clarifai_url(
+        self.workflow_url = ClarifaiUrlHelper().clarifai_url(
             wf.user_id, wf.app_id, "workflows", wf.id
         )
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds support for the Model Context Protocol (MCP), improves CLI tooling, and refines model-building scripts.

- Introduce `MCPModelClass` and a dummy MCP-powered sample model  
- Add an `env` CLI command and enhance `clarifai.cli.model.local_dev`  
- Update `ModelBuilder` to disable mocking on demand and generate MCP client snippets

### Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                       | Description                                                      |
| ---------------------------------------------------------- | ---------------------------------------------------------------- |
| tests/runners/dummy_mcp_model/requirements.txt             | Add dependencies needed for dummy MCP model                      |
| tests/runners/dummy_mcp_model/config.yaml                  | Provide a sample config for MCP models                          |
| tests/runners/dummy_mcp_model/1/model.py                   | Implement a sample MCP server and `MyModelClass`                |
| clarifai/utils/config.py                                   | Add `print_env_vars` helper for scripting                        |
| clarifai/runners/models/model_builder.py                   | Support disabling mocks and generate MCP client code snippet     |
| clarifai/runners/models/mcp_class.py                       | New base class wrapping FastMCP servers                          |
| clarifai/cli/model.py                                      | Log full model URL and disable mocking in `local_dev`            |
| clarifai/cli/base.py                                       | Add `config env` command to print environment variables          |
| clarifai/__init__.py                                        | Bump package version from 11.4.2 to 11.4.3rc1                   |
</details>



<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**tests/runners/dummy_mcp_model/1/model.py:12**
* The function name `sum` shadows the built-in `sum`; consider renaming it (e.g., `calculate_sum`) to avoid confusion.
```
def sum(a: Any = Field(description="first number"), b: Any = Field(description="second number")):
```
**clarifai/cli/model.py:532**
* [nitpick] Remove this outdated commented-out code block to keep the codebase clean.
```
# client_model = Model(
```
**clarifai/cli/model.py:508**
* This logs a relative path rather than the full URL including the host; consider using the complete `model_url` for clarity.
```
logger.info(f"Full url for the model: /users/{user_id}/apps/{app_id}/models/{model.id}/versions/{version.id}")
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T04:14:46Z
user: login: luv-bansal
body: Just some minor comments. Looking good to me!!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T06:40:04Z
user: login: luv-bansal
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T06:53:54Z
user: login: mogith-pn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T10:34:08Z
user: login: zeiler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T14:37:43Z
user: login: zeiler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T15:16:14Z
user: login: zeiler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T16:03:17Z

Review comments:
diff_hunk: @@ -0,0 +1,5 @@
+aiohttp
+requests
+clarifai
+huggingface_hub
path: tests/runners/dummy_mcp_model/requirements.txt
user: login: Copilot
body: Most dependencies except `fastmcp` are unpinned; consider specifying version ranges to avoid future incompatibilities.
```suggestion
aiohttp>=3.8.0,<4.0.0
requests>=2.26.0,<3.0.0
clarifai>=9.7.0,<10.0.0
huggingface_hub>=0.15.0,<1.0.0
```
created_at: 2025-05-22T04:14:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+# This is the sample config file for the GOT OCR2.O model.
path: tests/runners/dummy_mcp_model/config.yaml
user: login: Copilot
body: [nitpick] The header references 'GOT OCR2.O' but this is a dummy MCP sample; please update the comment to match this model context.
```suggestion
# This is the sample config file for the dummy MCP model.
```
created_at: 2025-05-22T04:14:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,6 +102,25 @@ def set_to_env(self):
                 envvar_name = 'CLARIFAI_' + envvar_name
             os.environ[envvar_name] = str(v)
 
+    def print_env_vars(self):
+        """prints the context env vars to the terminal
path: clarifai/utils/config.py
user: login: Copilot
body: [nitpick] Docstring should follow style guide: capitalize the first word and end with a period.
```suggestion
        """Prints the context env vars to the terminal.
```
created_at: 2025-05-22T04:14:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,6 +102,25 @@ def set_to_env(self):
                 envvar_name = 'CLARIFAI_' + envvar_name
             os.environ[envvar_name] = str(v)
 
+    def print_env_vars(self):
+        """prints the context env vars to the terminal
+
+        Example:
+          # This is helpful in scripts so you can do
+
+          from clarifai.utils.config import Config
+
+          Config.from_yaml().current.print_env_vars()
+
+        """
+        for k, v in self['env'].items():
path: clarifai/utils/config.py
user: login: Copilot
body: [nitpick] Consider sorting the environment variable keys before printing to ensure consistent output order.
```suggestion
        for k, v in sorted(self['env'].items()):
```
created_at: 2025-05-22T04:14:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -410,11 +410,11 @@ def method_signatures_yaml(self):
         signatures = {method.name: method.signature for method in method_info.values()}
         return signatures_to_yaml(signatures)
 
-    def get_method_signatures(self):
+    def get_method_signatures(self, mocking=True):
         """
         Returns the method signatures for the model class.
path: clarifai/runners/models/model_builder.py
user: login: Copilot
body: Please update the method docstring to describe the new `mocking` parameter and its default behavior.
```suggestion
        Returns the method signatures for the model class.

        Args:
          mocking (bool): If True (default), the method uses a mocked version of the model class
            for generating method signatures. If False, it uses the actual model class.

        Returns:
          list: A list of method signatures for the model class.
```
created_at: 2025-05-22T04:14:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,141 @@
+"""Base class for creating Model Context Protocol (MCP) servers."""
+
+import asyncio
+import json
+from typing import Any
+
+from fastmcp import Client, FastMCP  # use fastmcp v2 not the built in mcp
+from mcp import types
+from mcp.shared.exceptions import McpError
+
+from clarifai.runners.models.model_class import ModelClass
+
+# class MCPServerProvider(abc.ABC):
+#     """
+#     Base class for creating Model Context Protocol (MCP) servers.
+
+#     This class provides a base implementation of the MCP server, including
+#     methods for handling requests and responses, as well as error handling and
+#     logging.
+
+#     Attributes:
+#     _server: The FastMCP server instance.
+#     _tools: List of tools available in the server.
+#     _resources: List of resources available in the server.
+#     _prompts: List of prompts available in the server.
+
+#     Methods:
+#     get_server(): Returns the FastMCP server instance.
+#     mcp_transport(msg): Handles incoming messages and sends them to the FastMCP server.
+#     """
+
+#     @abc.abstractmethod
+#     def get_server(self) -> FastMCP:
+#         """Required method for each subclass to implement to return the FastMCP server to use."""
+#         if self._server is None:
+#             raise ValueError("Server not initialized")
+#         return self._server
+
+
+class MCPModelClass(ModelClass):
+    """Base class for wrapping FastMCP servers as a model running in Clarfai. This handles
+    all the transport between the API and the MCP server here. Simply subclass this and implement
+    the get_server() method to return the FastMCP server instance. The server is then used to
+    handle all the requests and responses.
+    """
+
+    def load_model(self):
+        # in memory transport provided in fastmcp v2 so we can easily use the client functions.
+        self.client = Client(self.get_server())
+
+    def get_server(self) -> FastMCP:
+        """Required method for each subclass to implement to return the FastMCP server to use."""
+        raise NotImplementedError("Subclasses must implement get_server() method")
+
+    @ModelClass.method
+    def mcp_transport(self, msg: str) -> str:
+        """The single model method to get the jsonrpc message and send it to the FastMCP server then
+        return it's response.
+
+        """
+
+        async def send_notification(client_message: types.ClientNotification) -> None:
+            async with self.client:
+                # Strip the jsonrpc field since send_notification will also pass it in for some reason.
+                client_message = types.ClientNotification.model_validate(
+                    client_message.model_dump(
+                        by_alias=True, mode="json", exclude_none=True, exclude={"jsonrpc"}
+                    )
+                )
+                try:
+                    return await self.client.session.send_notification(client_message)
+                except McpError as e:
+                    return types.JSONRPCError(jsonrpc="2.0", error=e.error)
+
+        async def send_request(client_message: types.ClientRequest, id: str) -> Any:
+            async with self.client:
+                # Strip the jsonrpc and id fields as send_request sets them again too.
+                client_message = types.ClientRequest.model_validate(
+                    client_message.model_dump(
+                        by_alias=True, mode="json", exclude_none=True, exclude={"jsonrpc", "id"}
+                    )
+                )
+
+                result_type = None
+                if isinstance(client_message.root, types.PingRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.InitializeRequest):
+                    return await self.client.session.initialize()
+                elif isinstance(client_message.root, types.SetLevelRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.ListResourcesRequest):
+                    result_type = types.ListResourcesResult
+                elif isinstance(client_message.root, types.ListResourceTemplatesRequest):
+                    result_type = types.ListResourceTemplatesResult
+                elif isinstance(client_message.root, types.ReadResourceRequest):
+                    result_type = types.ReadResourceResult
+                elif isinstance(client_message.root, types.SubscribeRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.UnsubscribeRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.ListPromptsRequest):
+                    result_type = types.ListPromptsResult
+                elif isinstance(client_message.root, types.GetPromptRequest):
+                    result_type = types.GetPromptResult
+                elif isinstance(client_message.root, types.CompleteRequest):
+                    result_type = types.CompleteResult
+                elif isinstance(client_message.root, types.ListToolsRequest):
+                    result_type = types.ListToolsResult
+                elif isinstance(client_message.root, types.CallToolRequest):
+                    result_type = types.CallToolResult
+                else:
+                    # this is a special case where we need to return the list of tools.
+                    raise NotImplementedError(f"Method {client_message.method} not implemented")
+                # Call the mcp server using send_request() or send_notification() depending on the method.
+                try:
+                    return await self.client.session.send_request(client_message, result_type)
+                except McpError as e:
+                    return types.JSONRPCError(jsonrpc="2.0", id=id, error=e.error)
+
+        # The message coming here is the generic request. We look at it's .method
+        # to determine which client function to call and to further subparse the params.
+        # Note(zeiler): unfortunately the pydantic types in mcp/types.py are not consistent.
+        # The JSONRPCRequest are supposed to have an id but the InitializeRequest
+        # does not have it.
+        d = json.loads(msg)
+
+        # If we have an id it's a JSONRPCRequest
+        if not d.get('method', None).startswith("notifications/"):
path: clarifai/runners/models/mcp_class.py
user: login: Copilot
body: If `method` is missing, `d.get` returns `None` and calling `startswith` will raise an exception; use `d.get('method', '')` or guard against `None` first.
```suggestion
        if not d.get('method', '').startswith("notifications/"):
```
created_at: 2025-05-22T04:14:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -143,6 +143,14 @@ def dump(ctx_obj, output_format):
         json.dump(ctx_obj.to_dict(), sys.stdout, indent=2)
 
 
+@config.command(['cat'])
+@click.option('-o', '--output-format', default='yaml', type=click.Choice(['yaml', 'json']))
path: clarifai/cli/base.py
user: login: Copilot
body: The `output_format` option is declared but not used in `env`; either leverage it to format output or remove the option.
created_at: 2025-05-22T04:14:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -934,18 +934,43 @@ def filter_func(tarinfo):
         try:
             is_uploaded = self.monitor_model_build()
             if is_uploaded:
-                from clarifai.runners.utils import code_script
-
-                method_signatures = self.get_method_signatures()
-                snippet = code_script.generate_client_script(
-                    method_signatures,
-                    user_id=self.client.user_app_id.user_id,
-                    app_id=self.client.user_app_id.app_id,
-                    model_id=self.model_proto.id,
-                )
+                # Provide an mcp client config
+                if model_type_id == "mcp":
+                    snippet = (
+                        """
+import asyncio
+import os
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+
+transport = StreamableHttpTransport(url="%s/mcp",
+                                    headers={"Authorization": "Bearer " + os.environ["CLARIFAI_PAT"]})
+
+async def main():
+  async with Client(transport) as client:
+    tools = await client.list_tools()
+    print(f"Available tools: {tools}")
+    result = await client.call_tool(tools[0].name, {"a": 5, "b": 3})
+    print(f"Result: {result[0].text}")
+
+if __name__ == "__main__":
+  asyncio.run(main())
+"""
path: clarifai/runners/models/model_builder.py
user: login: luv-bansal
body: I think this logic should be in `clarifai/runners/utils/code_script.py` file and update the `code_script.generate_client_script()` method instead
created_at: 2025-05-22T05:47:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -143,6 +143,14 @@ def dump(ctx_obj, output_format):
         json.dump(ctx_obj.to_dict(), sys.stdout, indent=2)
 
 
+@config.command(['cat'])
+@click.option('-o', '--output-format', default='yaml', type=click.Choice(['yaml', 'json']))
path: clarifai/cli/base.py
user: login: luv-bansal
body: agree with this comment 
created_at: 2025-05-22T06:38:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,141 @@
+"""Base class for creating Model Context Protocol (MCP) servers."""
+
+import asyncio
+import json
+from typing import Any
+
+from fastmcp import Client, FastMCP  # use fastmcp v2 not the built in mcp
+from mcp import types
+from mcp.shared.exceptions import McpError
path: clarifai/runners/models/mcp_class.py
user: login: luv-bansal
body: Should we include the `mc`p dependency in `requirements.txt`? Not entirely sure — it might not be necessary, but we should test it to confirm.
created_at: 2025-05-22T06:47:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,141 @@
+"""Base class for creating Model Context Protocol (MCP) servers."""
+
+import asyncio
+import json
+from typing import Any
+
+from fastmcp import Client, FastMCP  # use fastmcp v2 not the built in mcp
+from mcp import types
+from mcp.shared.exceptions import McpError
+
+from clarifai.runners.models.model_class import ModelClass
+
+# class MCPServerProvider(abc.ABC):
+#     """
+#     Base class for creating Model Context Protocol (MCP) servers.
+
+#     This class provides a base implementation of the MCP server, including
+#     methods for handling requests and responses, as well as error handling and
+#     logging.
+
+#     Attributes:
+#     _server: The FastMCP server instance.
+#     _tools: List of tools available in the server.
+#     _resources: List of resources available in the server.
+#     _prompts: List of prompts available in the server.
+
+#     Methods:
+#     get_server(): Returns the FastMCP server instance.
+#     mcp_transport(msg): Handles incoming messages and sends them to the FastMCP server.
+#     """
+
+#     @abc.abstractmethod
+#     def get_server(self) -> FastMCP:
+#         """Required method for each subclass to implement to return the FastMCP server to use."""
+#         if self._server is None:
+#             raise ValueError("Server not initialized")
+#         return self._server
+
+
+class MCPModelClass(ModelClass):
+    """Base class for wrapping FastMCP servers as a model running in Clarfai. This handles
+    all the transport between the API and the MCP server here. Simply subclass this and implement
+    the get_server() method to return the FastMCP server instance. The server is then used to
+    handle all the requests and responses.
+    """
+
+    def load_model(self):
+        # in memory transport provided in fastmcp v2 so we can easily use the client functions.
+        self.client = Client(self.get_server())
+
+    def get_server(self) -> FastMCP:
+        """Required method for each subclass to implement to return the FastMCP server to use."""
+        raise NotImplementedError("Subclasses must implement get_server() method")
+
+    @ModelClass.method
+    def mcp_transport(self, msg: str) -> str:
+        """The single model method to get the jsonrpc message and send it to the FastMCP server then
+        return it's response.
+
+        """
+
+        async def send_notification(client_message: types.ClientNotification) -> None:
+            async with self.client:
+                # Strip the jsonrpc field since send_notification will also pass it in for some reason.
+                client_message = types.ClientNotification.model_validate(
+                    client_message.model_dump(
+                        by_alias=True, mode="json", exclude_none=True, exclude={"jsonrpc"}
+                    )
+                )
+                try:
+                    return await self.client.session.send_notification(client_message)
+                except McpError as e:
+                    return types.JSONRPCError(jsonrpc="2.0", error=e.error)
+
+        async def send_request(client_message: types.ClientRequest, id: str) -> Any:
+            async with self.client:
+                # Strip the jsonrpc and id fields as send_request sets them again too.
+                client_message = types.ClientRequest.model_validate(
+                    client_message.model_dump(
+                        by_alias=True, mode="json", exclude_none=True, exclude={"jsonrpc", "id"}
+                    )
+                )
+
+                result_type = None
+                if isinstance(client_message.root, types.PingRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.InitializeRequest):
+                    return await self.client.session.initialize()
+                elif isinstance(client_message.root, types.SetLevelRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.ListResourcesRequest):
+                    result_type = types.ListResourcesResult
+                elif isinstance(client_message.root, types.ListResourceTemplatesRequest):
+                    result_type = types.ListResourceTemplatesResult
+                elif isinstance(client_message.root, types.ReadResourceRequest):
+                    result_type = types.ReadResourceResult
+                elif isinstance(client_message.root, types.SubscribeRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.UnsubscribeRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.ListPromptsRequest):
+                    result_type = types.ListPromptsResult
+                elif isinstance(client_message.root, types.GetPromptRequest):
+                    result_type = types.GetPromptResult
+                elif isinstance(client_message.root, types.CompleteRequest):
+                    result_type = types.CompleteResult
+                elif isinstance(client_message.root, types.ListToolsRequest):
+                    result_type = types.ListToolsResult
+                elif isinstance(client_message.root, types.CallToolRequest):
+                    result_type = types.CallToolResult
+                else:
+                    # this is a special case where we need to return the list of tools.
+                    raise NotImplementedError(f"Method {client_message.method} not implemented")
+                # Call the mcp server using send_request() or send_notification() depending on the method.
+                try:
+                    return await self.client.session.send_request(client_message, result_type)
+                except McpError as e:
+                    return types.JSONRPCError(jsonrpc="2.0", id=id, error=e.error)
+
+        # The message coming here is the generic request. We look at it's .method
+        # to determine which client function to call and to further subparse the params.
+        # Note(zeiler): unfortunately the pydantic types in mcp/types.py are not consistent.
+        # The JSONRPCRequest are supposed to have an id but the InitializeRequest
+        # does not have it.
+        d = json.loads(msg)
+
+        # If we have an id it's a JSONRPCRequest
+        if not d.get('method', None).startswith("notifications/"):
+            # rpc_message2 = types.JSONRPCRequest.model_validate(rpc_message)
+            # underlying: types.JSONRPCRequest = rpc_message.root
+            client_message = types.ClientRequest.model_validate(d)
+            response = asyncio.run(
+                send_request(client_message, id=d.get('id', ""))
+            )  # underlying.id))
+        else:  # JSONRPCRequest
+            client_message = types.ClientNotification.model_validate(d)
+            response = asyncio.run(send_notification(client_message))
+        if response is None:
+            return ""
path: clarifai/runners/models/mcp_class.py
user: login: mogith-pn
body: Just a suggestion,
Can we add a jsonRPC error response here instead of empty string ? Will it be helpful for the users when no response is received from the mcp server?
created_at: 2025-05-22T10:34:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,141 @@
+"""Base class for creating Model Context Protocol (MCP) servers."""
+
+import asyncio
+import json
+from typing import Any
+
+from fastmcp import Client, FastMCP  # use fastmcp v2 not the built in mcp
+from mcp import types
+from mcp.shared.exceptions import McpError
path: clarifai/runners/models/mcp_class.py
user: login: zeiler
body: right that's what I was mentioning on slack, if the user doesn't import from mcp_class.py then we don't need to add that to the default requirements. maybe better for now then they add it to their model requirements. We'll need to add a bunch of MCP examples next. 
created_at: 2025-05-22T14:37:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,141 @@
+"""Base class for creating Model Context Protocol (MCP) servers."""
+
+import asyncio
+import json
+from typing import Any
+
+from fastmcp import Client, FastMCP  # use fastmcp v2 not the built in mcp
+from mcp import types
+from mcp.shared.exceptions import McpError
+
+from clarifai.runners.models.model_class import ModelClass
+
+# class MCPServerProvider(abc.ABC):
+#     """
+#     Base class for creating Model Context Protocol (MCP) servers.
+
+#     This class provides a base implementation of the MCP server, including
+#     methods for handling requests and responses, as well as error handling and
+#     logging.
+
+#     Attributes:
+#     _server: The FastMCP server instance.
+#     _tools: List of tools available in the server.
+#     _resources: List of resources available in the server.
+#     _prompts: List of prompts available in the server.
+
+#     Methods:
+#     get_server(): Returns the FastMCP server instance.
+#     mcp_transport(msg): Handles incoming messages and sends them to the FastMCP server.
+#     """
+
+#     @abc.abstractmethod
+#     def get_server(self) -> FastMCP:
+#         """Required method for each subclass to implement to return the FastMCP server to use."""
+#         if self._server is None:
+#             raise ValueError("Server not initialized")
+#         return self._server
+
+
+class MCPModelClass(ModelClass):
+    """Base class for wrapping FastMCP servers as a model running in Clarfai. This handles
+    all the transport between the API and the MCP server here. Simply subclass this and implement
+    the get_server() method to return the FastMCP server instance. The server is then used to
+    handle all the requests and responses.
+    """
+
+    def load_model(self):
+        # in memory transport provided in fastmcp v2 so we can easily use the client functions.
+        self.client = Client(self.get_server())
+
+    def get_server(self) -> FastMCP:
+        """Required method for each subclass to implement to return the FastMCP server to use."""
+        raise NotImplementedError("Subclasses must implement get_server() method")
+
+    @ModelClass.method
+    def mcp_transport(self, msg: str) -> str:
+        """The single model method to get the jsonrpc message and send it to the FastMCP server then
+        return it's response.
+
+        """
+
+        async def send_notification(client_message: types.ClientNotification) -> None:
+            async with self.client:
+                # Strip the jsonrpc field since send_notification will also pass it in for some reason.
+                client_message = types.ClientNotification.model_validate(
+                    client_message.model_dump(
+                        by_alias=True, mode="json", exclude_none=True, exclude={"jsonrpc"}
+                    )
+                )
+                try:
+                    return await self.client.session.send_notification(client_message)
+                except McpError as e:
+                    return types.JSONRPCError(jsonrpc="2.0", error=e.error)
+
+        async def send_request(client_message: types.ClientRequest, id: str) -> Any:
+            async with self.client:
+                # Strip the jsonrpc and id fields as send_request sets them again too.
+                client_message = types.ClientRequest.model_validate(
+                    client_message.model_dump(
+                        by_alias=True, mode="json", exclude_none=True, exclude={"jsonrpc", "id"}
+                    )
+                )
+
+                result_type = None
+                if isinstance(client_message.root, types.PingRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.InitializeRequest):
+                    return await self.client.session.initialize()
+                elif isinstance(client_message.root, types.SetLevelRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.ListResourcesRequest):
+                    result_type = types.ListResourcesResult
+                elif isinstance(client_message.root, types.ListResourceTemplatesRequest):
+                    result_type = types.ListResourceTemplatesResult
+                elif isinstance(client_message.root, types.ReadResourceRequest):
+                    result_type = types.ReadResourceResult
+                elif isinstance(client_message.root, types.SubscribeRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.UnsubscribeRequest):
+                    result_type = types.EmptyResult
+                elif isinstance(client_message.root, types.ListPromptsRequest):
+                    result_type = types.ListPromptsResult
+                elif isinstance(client_message.root, types.GetPromptRequest):
+                    result_type = types.GetPromptResult
+                elif isinstance(client_message.root, types.CompleteRequest):
+                    result_type = types.CompleteResult
+                elif isinstance(client_message.root, types.ListToolsRequest):
+                    result_type = types.ListToolsResult
+                elif isinstance(client_message.root, types.CallToolRequest):
+                    result_type = types.CallToolResult
+                else:
+                    # this is a special case where we need to return the list of tools.
+                    raise NotImplementedError(f"Method {client_message.method} not implemented")
+                # Call the mcp server using send_request() or send_notification() depending on the method.
+                try:
+                    return await self.client.session.send_request(client_message, result_type)
+                except McpError as e:
+                    return types.JSONRPCError(jsonrpc="2.0", id=id, error=e.error)
+
+        # The message coming here is the generic request. We look at it's .method
+        # to determine which client function to call and to further subparse the params.
+        # Note(zeiler): unfortunately the pydantic types in mcp/types.py are not consistent.
+        # The JSONRPCRequest are supposed to have an id but the InitializeRequest
+        # does not have it.
+        d = json.loads(msg)
+
+        # If we have an id it's a JSONRPCRequest
+        if not d.get('method', None).startswith("notifications/"):
+            # rpc_message2 = types.JSONRPCRequest.model_validate(rpc_message)
+            # underlying: types.JSONRPCRequest = rpc_message.root
+            client_message = types.ClientRequest.model_validate(d)
+            response = asyncio.run(
+                send_request(client_message, id=d.get('id', ""))
+            )  # underlying.id))
+        else:  # JSONRPCRequest
+            client_message = types.ClientNotification.model_validate(d)
+            response = asyncio.run(send_notification(client_message))
+        if response is None:
+            return ""
path: clarifai/runners/models/mcp_class.py
user: login: zeiler
body: good idea, added
created_at: 2025-05-22T15:16:14Z
author_association: MEMBER
diff_hunk: @@ -934,18 +934,43 @@ def filter_func(tarinfo):
         try:
             is_uploaded = self.monitor_model_build()
             if is_uploaded:
-                from clarifai.runners.utils import code_script
-
-                method_signatures = self.get_method_signatures()
-                snippet = code_script.generate_client_script(
-                    method_signatures,
-                    user_id=self.client.user_app_id.user_id,
-                    app_id=self.client.user_app_id.app_id,
-                    model_id=self.model_proto.id,
-                )
+                # Provide an mcp client config
+                if model_type_id == "mcp":
+                    snippet = (
+                        """
+import asyncio
+import os
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+
+transport = StreamableHttpTransport(url="%s/mcp",
+                                    headers={"Authorization": "Bearer " + os.environ["CLARIFAI_PAT"]})
+
+async def main():
+  async with Client(transport) as client:
+    tools = await client.list_tools()
+    print(f"Available tools: {tools}")
+    result = await client.call_tool(tools[0].name, {"a": 5, "b": 3})
+    print(f"Result: {result[0].text}")
+
+if __name__ == "__main__":
+  asyncio.run(main())
+"""
path: clarifai/runners/models/model_builder.py
user: login: zeiler
body: moved it in there, had to be kind of hacky with matching the mcp_transport function name in the signatures since the model_type_id doesn't seem to be present everywhere.
created_at: 2025-05-22T16:03:17Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-22T18:12:48Z
body: ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-66%25-yellow?style=flat)

Package | Line Rate | Health
-------- | --------- | ------
clarifai | 43% | ❌
clarifai.cli | 43% | ❌
clarifai.client | 71% | ➖
clarifai.client.auth | 74% | ➖
clarifai.constants | 100% | ✔
clarifai.datasets | 100% | ✔
clarifai.datasets.export | 80% | ✔
clarifai.datasets.upload | 75% | ➖
clarifai.datasets.upload.loaders | 37% | ❌
clarifai.models | 100% | ✔
clarifai.modules | 0% | ❌
clarifai.rag | 72% | ➖
clarifai.runners | 10% | ❌
clarifai.runners.models | 58% | ➖
clarifai.runners.utils | 63% | ➖
clarifai.runners.utils.data_types | 77% | ➖
clarifai.schema | 100% | ✔
clarifai.urls | 75% | ➖
clarifai.utils | 73% | ➖
clarifai.utils.evaluation | 67% | ➖
clarifai.workflows | 94% | ✔
**Summary** | **66%** (6144 / 9330) | ➖

_Minimum allowed line rate is `50%`_

<!-- Sticky Pull Request Comment -->
author_association: NONE