Skip to main content

A transport-agnostic kernel for MCP servers

Project description

MCPK

A transport-agnostic kernel for MCP servers.

Register tools, resources, and prompts with type-safe handlers. Add hooks for permissions, validation, and observability. MCPK handles execution without dictating how you handle transport (stdio, HTTP, WebSocket, etc.).

Installation

pip install mcpk

Requires Python 3.12+. Zero runtime dependencies.

For JSON Schema validation (strict mode):

pip install mcpk[validation]

Quick Start

Synchronous Kernel

from mcpk import Kernel, ToolDef, ToolResult, TextItem, ExecutionScope

# Create a kernel
kernel = Kernel[dict]()

# Define and register a tool
tool_def = ToolDef(
    name="greet",
    description="Greet a user",
    input_schema={
        "type": "object",
        "properties": {"name": {"type": "string"}},
        "required": ["name"],
    },
)

def greet_handler(scope: ExecutionScope[dict], args: dict) -> ToolResult:
    return ToolResult(content=(TextItem(text=f"Hello, {args['name']}!"),))

kernel.register_tool(tool_def, greet_handler)

# Call the tool
scope = ExecutionScope(ctx={"user_id": "123"})
result = kernel.call_tool("greet", {"name": "Alice"}, scope)
print(result.content[0].text)  # "Hello, Alice!"

Async Kernel

import asyncio
from mcpk import AsyncKernel, ToolDef, ToolResult, TextItem, ExecutionScope

kernel = AsyncKernel[None]()

tool_def = ToolDef(
    name="fetch_data",
    input_schema={"type": "object", "properties": {}},
)

async def fetch_handler(scope: ExecutionScope[None], args: dict) -> ToolResult:
    await asyncio.sleep(0.1)  # Simulate async work
    return ToolResult(content=(TextItem(text="Data fetched"),))

kernel.register_tool(tool_def, fetch_handler)

async def main():
    result = await kernel.call_tool("fetch_data", {}, ExecutionScope(ctx=None))
    print(result.content[0].text)

asyncio.run(main())

Registering Capabilities

Tools

Tools are functions that perform actions and return results.

from mcpk import ToolDef, ToolResult, TextItem, ImageItem

# Simple tool
tool = ToolDef(
    name="calculate",
    description="Perform a calculation",
    input_schema={
        "type": "object",
        "properties": {
            "expression": {"type": "string"},
        },
        "required": ["expression"],
    },
)

def calculate(scope, args):
    result = eval(args["expression"])  # Don't do this in production!
    return ToolResult(content=(TextItem(text=str(result)),))

kernel.register_tool(tool, calculate)

# Tool returning multiple content types
def screenshot(scope, args):
    return ToolResult(content=(
        TextItem(text="Screenshot captured"),
        ImageItem(data=b"...", mime_type="image/png"),
    ))

Resources

Resources provide read-only data access.

from mcpk import ResourceDef, ResourceResult
from mcpk.types import ResourceContent

resource = ResourceDef(
    uri="file:///config.json",
    name="Configuration",
    description="Application configuration",
    mime_type="application/json",
)

def read_config(scope, uri):
    return ResourceResult(contents=(
        ResourceContent(uri=uri, text='{"debug": true}', mime_type="application/json"),
    ))

kernel.register_resource(resource, read_config)

# Read the resource
result = kernel.read_resource("file:///config.json", scope)

Prompts

Prompts are reusable message templates.

from mcpk import PromptDef, PromptResult
from mcpk.types import PromptMessage, PromptArgumentDef

prompt = PromptDef(
    name="code_review",
    description="Review code for issues",
    arguments=(
        PromptArgumentDef(name="code", required=True),
        PromptArgumentDef(name="language", description="Programming language"),
    ),
)

def code_review_prompt(scope, args):
    return PromptResult(messages=(
        PromptMessage(
            role="user",
            content=TextItem(text=f"Review this {args.get('language', 'code')}:\n{args['code']}"),
        ),
    ))

kernel.register_prompt(prompt, code_review_prompt)

# Get the prompt
result = kernel.get_prompt("code_review", {"code": "print('hi')", "language": "Python"}, scope)

Hooks

Hooks allow you to inject custom behavior for permissions, validation, and event handling.

Permission Hook

Control access to tools, resources, and prompts.

from mcpk import Kernel
from mcpk.hooks import PermissionRequest
from mcpk.errors import PermissionDeniedError

def permission_hook(scope, request: PermissionRequest):
    # request.kind: "tool" | "resource" | "prompt"
    # request.name: tool name, resource URI, or prompt name
    # request.arguments: arguments for tools/prompts (None for resources)

    if request.kind == "tool" and request.name == "dangerous_tool":
        if not scope.ctx.get("admin"):
            raise PermissionDeniedError("Admin access required")

kernel = Kernel[dict](permission_hook=permission_hook)

Validation Hook

Validate tool arguments with custom logic.

from mcpk import Kernel
from mcpk.errors import ValidationError

def validation_hook(tool_name: str, arguments: dict, schema: dict):
    # Custom validation logic
    if "forbidden_key" in arguments:
        raise ValidationError("forbidden_key is not allowed")

kernel = Kernel[None](validation_hook=validation_hook)

Strict Mode

Enable built-in JSON Schema validation with strict=True. Requires pip install mcpk[validation].

from mcpk import Kernel, ToolDef

# Strict mode validates:
# 1. Tool schemas are valid JSON Schema at registration
# 2. Tool arguments match schemas at invocation
kernel = Kernel[None](strict=True)

tool = ToolDef(
    name="greet",
    input_schema={
        "type": "object",
        "properties": {"name": {"type": "string"}},
        "required": ["name"],
    },
)
kernel.register_tool(tool, handler)

# This raises ValidationError - missing required "name"
kernel.call_tool("greet", {}, scope)

# This raises ValidationError - wrong type for "name"
kernel.call_tool("greet", {"name": 123}, scope)

Strict mode validation runs before any custom validation_hook, so both can be used together.

Event Handler

Observe tool calls, resource reads, and prompt gets.

from mcpk import Kernel
from mcpk.events import Event, ToolCallEvent, LogEvent, ProgressEvent

def event_handler(event: Event):
    match event:
        case ToolCallEvent(phase="before", tool_name=name):
            print(f"Calling tool: {name}")
        case ToolCallEvent(phase="after", result=result):
            print(f"Tool completed: {result}")
        case ToolCallEvent(phase="error", error=err):
            print(f"Tool failed: {err}")
        case LogEvent(level=level, data=data):
            print(f"[{level}] {data}")
        case ProgressEvent(progress=p, total=t):
            print(f"Progress: {p}/{t}")

kernel = Kernel[None](event_handler=event_handler)

Emitting Events from Handlers

Handlers can emit progress and log events via the kernel.

def long_running_tool(scope, args):
    kernel.emit_progress(scope, 0, total=100, message="Starting...")
    # ... do work ...
    kernel.emit_progress(scope, 50, total=100, message="Halfway done")
    # ... more work ...
    kernel.emit_log("info", {"step": "completed", "items": 42})
    return ToolResult(content=(TextItem(text="Done"),))

Note: Progress events require scope.progress_token to be set.

Listing Capabilities

# Get all registered definitions
tools = kernel.all_tools()          # tuple[ToolDef, ...]
resources = kernel.all_resources()  # tuple[ResourceDef, ...]
prompts = kernel.all_prompts()      # tuple[PromptDef, ...]

Error Handling

All errors inherit from McpkError with JSON-RPC compatible error codes.

from mcpk.errors import (
    McpkError,              # Base error
    ToolNotFoundError,      # Tool not registered
    ResourceNotFoundError,  # Resource not registered
    PromptNotFoundError,    # Prompt not registered
    ValidationError,        # Invalid arguments
    SpecError,              # MCP spec violation
    PermissionDeniedError,  # Access denied by hook
    ExecutionError,         # Handler raised an exception
)

try:
    result = kernel.call_tool("nonexistent", {}, scope)
except ToolNotFoundError as e:
    print(f"Error code: {e.code}")  # -32601 (METHOD_NOT_FOUND)
    print(f"Tool: {e.name}")
except ExecutionError as e:
    print(f"Handler failed: {e}")
    print(f"Cause: {e.__cause__}")

Type-Safe Context

The kernel is generic over your context type:

from dataclasses import dataclass

@dataclass
class UserContext:
    user_id: str
    permissions: list[str]

kernel = Kernel[UserContext]()

def secure_tool(scope: ExecutionScope[UserContext], args):
    if "admin" not in scope.ctx.permissions:
        return ToolResult(content=(TextItem(text="Access denied"),), is_error=True)
    return ToolResult(content=(TextItem(text="Secret data"),))

Building Context from Requests

Since MCPK is transport-agnostic, you build the context in your transport layer before calling the kernel. Here's a pattern for HTTP:

from dataclasses import dataclass
from mcpk import Kernel, ExecutionScope

@dataclass
class RequestContext:
    user_id: str
    permissions: list[str]
    request_id: str

kernel = Kernel[RequestContext]()

# Context factory - called per request in your transport layer
def build_context(headers: dict, request_id: str) -> ExecutionScope[RequestContext]:
    # Extract user from auth token, session, etc.
    auth_token = headers.get("Authorization", "")
    user = validate_token(auth_token)  # Your auth logic

    return ExecutionScope(
        ctx=RequestContext(
            user_id=user.id,
            permissions=user.permissions,
            request_id=request_id,
        ),
        request_id=request_id,
    )

# In your HTTP handler
def handle_tool_call(request):
    scope = build_context(request.headers, request.id)
    result = kernel.call_tool(request.tool_name, request.arguments, scope)
    return result

The permission hook can then use context for access control:

def permission_hook(scope: ExecutionScope[RequestContext], request: PermissionRequest):
    if request.kind == "tool" and request.name == "admin_tool":
        if "admin" not in scope.ctx.permissions:
            raise PermissionDeniedError(f"User {scope.ctx.user_id} lacks admin permission")

kernel = Kernel[RequestContext](permission_hook=permission_hook)

Public API Reference

Main Exports (mcpk)

Export Description
Kernel Synchronous kernel
AsyncKernel Asynchronous kernel
ExecutionScope Wraps context with execution metadata
ToolDef Tool definition
ResourceDef Resource definition
PromptDef Prompt definition
ToolResult Tool execution result
ResourceResult Resource read result
PromptResult Prompt get result
TextItem Text content
ImageItem Image content
AudioItem Audio content
EmbeddedResourceItem Embedded resource
ResourceLinkItem Resource link
ContentItem Union of content types

Additional Types (mcpk.types)

Type Description
ToolAnnotationsDef Tool annotation hints
PromptArgumentDef Prompt argument definition
ResourceContent Resource content (text or blob)
PromptMessage Message in prompt result
ToolHandler / AsyncToolHandler Handler type aliases
ResourceHandler / AsyncResourceHandler Handler type aliases
PromptHandler / AsyncPromptHandler Handler type aliases

Hooks (mcpk.hooks)

Type Description
PermissionRequest Details about permission being requested
PermissionHook / AsyncPermissionHook Permission hook type aliases
ValidationHook / AsyncValidationHook Validation hook type aliases

Events (mcpk.events)

Type Description
ToolCallEvent Before/after/error tool execution
ResourceReadEvent Before/after/error resource read
PromptGetEvent Before/after/error prompt get
ProgressEvent Progress notification
LogEvent Log notification
Event Union of event types
EventHandler / AsyncEventHandler Handler type aliases
LogLevel Log severity levels

Errors (mcpk.errors)

Error Code Description
McpkError - Base error class
ToolNotFoundError -32601 Tool not registered
ResourceNotFoundError -32601 Resource not registered
PromptNotFoundError -32601 Prompt not registered
ValidationError -32602 Invalid arguments
SpecError -32602 MCP spec violation
PermissionDeniedError -32603 Access denied
ExecutionError -32603 Handler exception

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcpk-0.1.0.tar.gz (65.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcpk-0.1.0-py3-none-any.whl (18.3 kB view details)

Uploaded Python 3

File details

Details for the file mcpk-0.1.0.tar.gz.

File metadata

  • Download URL: mcpk-0.1.0.tar.gz
  • Upload date:
  • Size: 65.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for mcpk-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3ef3863399f1c91f413a1e1dd4350dbd72ea1bb8356d5c6baddcc67ae830564d
MD5 3a4f390749f56c214d527a2a21a34da6
BLAKE2b-256 12f38461f4f92d663887caa98c4b35a7492971398c50eb145d6259592052c37e

See more details on using hashes here.

File details

Details for the file mcpk-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: mcpk-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for mcpk-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6174ade2cd98c1868faa0647782ff628225d47278e1f7bf5f21f4cbeb7dc78f5
MD5 b327a46a61cc6fe1572d971954471dc4
BLAKE2b-256 3e2b0baa6fa2712428b34c4cb6bad57eba15147d39b6501cdb6c4191c0cd67aa

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page