Metadata-Version: 2.4
Name: ethisyscore-plugin-sdk
Version: 1.0.0a3
Summary: EthisysCore Plugin SDK for Python — build container-hosted plugins
License: MIT
Keywords: dapr,ethisyscore,extension,mcp,plugin
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.115.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.10.0
Requires-Dist: uvicorn[standard]>=0.32.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# EthisysCore Python Plugin SDK

`ethisyscore-plugin-sdk` — Build **container-mode** plugins with Python 3.11+.

Container plugins run as isolated Docker containers with Dapr sidecars, communicating with the EthisysCore platform via HTTP. Built on FastAPI and Pydantic.

## Installation

```bash
pip install ethisyscore-plugin-sdk
```

## Quick Start

```python
from ethisyscore_plugin_sdk import (
    PluginBase, PluginHost, PluginManifest, McpToolDefinition,
    ToolResult, PluginContext,
)

class GreeterPlugin(PluginBase):
    @property
    def manifest(self) -> PluginManifest:
        return PluginManifest(
            id="greeter",
            name="Greeter Plugin",
            version="1.0.0",
            type="TenantPlugin",
            owner="acme-corp",
            compatible_core_version=">=1.0.0",
            mcp_version="1.0",
            execution_mode="Container",
            mcp_tools=[
                McpToolDefinition(
                    name="greet",
                    description="Say hello",
                    input_schema_json='{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}'
                )
            ],
        )

    async def on_invoke_tool(self, tool_name: str, args: dict, context: PluginContext) -> ToolResult:
        if tool_name == "greet":
            name = args.get("name", "World")
            return ToolResult(success=True, result_json=f'{{"greeting": "Hello, {name}!"}}')
        return ToolResult(success=False, error=f"Unknown tool: {tool_name}")

if __name__ == "__main__":
    PluginHost(GreeterPlugin()).start()
```

## Lifecycle Hooks

All hooks are optional — override only what you need:

```python
async def on_initialize(self, context: PluginContext) -> None     # After platform connects
async def on_enable(self, context: PluginContext) -> None         # Extension enabled for tenant
async def on_disable(self, context: PluginContext) -> None        # Extension disabled
async def on_get_resource(self, uri: str, context: PluginContext) -> ResourceResult
async def on_check_health(self) -> HealthResult                   # Health probe
```

## Platform Services

Available through `PluginContext`:

```python
# Key-value persistence (per plugin per tenant)
count = (await context.data_store.get("counter")) or 0
await context.data_store.set("counter", count + 1)

# Structured logging (synchronous — no await needed)
context.logger.info("Processing request")
context.logger.error("Something failed")

# Invoke tools from other plugins
result = await context.mcp_client.invoke_tool("other-tool", '{"key": "value"}')

# Publish domain events
await context.publish_event("ItemCreated", {"id": "123"})
```

## Type Safety

Protocol-based typing for IDE autocompletion:

- `PluginDataStoreProtocol` — `get()`, `set()`, `delete()`, `exists()`, `list_keys()`, `query()`
- `PluginLoggerProtocol` — `info()`, `warning()`, `error()`, `debug()`
- `McpClientProtocol` — `invoke_tool(tool_name, arguments_json)`, `list_tools()`, `get_resource()`, `list_resources()`

Manifest fields accept both `snake_case` and `camelCase` (Pydantic `populate_by_name`).

## HTTP Endpoints

`PluginHost` starts a Uvicorn/FastAPI server (port: `DAPR_APP_PORT` or 8080):

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/plugin/initialize` | POST | Platform connects, provides context |
| `/plugin/enable` | POST | Extension enabled for tenant |
| `/plugin/disable` | POST | Extension disabled |
| `/plugin/invoke-tool` | POST | MCP tool invocation |
| `/plugin/get-resource` | POST | MCP resource read |
| `/plugin/health` | GET | Health probe |
| `/plugin/heartbeat` | GET | Liveness check |

## Environment Variables

| Variable | Purpose | Default |
|----------|---------|---------|
| `DAPR_APP_PORT` | Port the plugin listens on | `8080` |
| `DAPR_HTTP_PORT` | Dapr sidecar HTTP port | `3500` |
| `DAPR_API_TOKEN` | Dapr sidecar auth token | — |

## v1 Limitations

Compared to the .NET SDK:

- **Monolithic dispatch** — all tools route through `on_invoke_tool()` (no handler-per-tool classes)
- **No middleware pipeline** — no built-in error handling, logging, or validation middleware
- **No DI container** — plugins manage their own dependencies

Handler-per-tool, middleware, and DI are planned for v2.

## Development

```bash
pip install -e ".[dev]"
pytest
```

Dependencies: `fastapi>=0.115.0`, `uvicorn[standard]>=0.32.0`, `httpx>=0.27.0`, `pydantic>=2.10.0`
