Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ ARCHON_AGENTS_PORT=8052
ARCHON_UI_PORT=3737
ARCHON_DOCS_PORT=3838

# MCP Transport Configuration
# Control which transport protocols the MCP server exposes
# ARCHON_MCP_ENABLE_STREAMABLE_HTTP: Enable Streamable HTTP transport at /mcp (recommended)
# ARCHON_MCP_ENABLE_SSE: Enable SSE transport at /sse (legacy support)
# Both default to "true" for maximum compatibility. At least one must be enabled.
ARCHON_MCP_ENABLE_STREAMABLE_HTTP=true
ARCHON_MCP_ENABLE_SSE=true

# Frontend Configuration
# VITE_ALLOWED_HOSTS: Comma-separated list of additional hosts allowed for Vite dev server
# Example: VITE_ALLOWED_HOSTS=192.168.1.100,myhost.local,example.com
Expand Down
335 changes: 59 additions & 276 deletions CLAUDE.md

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions archon-ui-main/public/img/LM-Studio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 18 additions & 7 deletions archon-ui-main/src/components/settings/RAGSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { credentialsService } from '../../services/credentialsService';
import OllamaModelDiscoveryModal from './OllamaModelDiscoveryModal';
import OllamaModelSelectionModal from './OllamaModelSelectionModal';

type ProviderKey = 'openai' | 'google' | 'ollama' | 'anthropic' | 'grok' | 'openrouter';
type ProviderKey = 'openai' | 'google' | 'ollama' | 'anthropic' | 'grok' | 'openrouter' | 'lmstudio';

// Providers that support embedding models
const EMBEDDING_CAPABLE_PROVIDERS: ProviderKey[] = ['openai', 'google', 'ollama'];
const EMBEDDING_CAPABLE_PROVIDERS: ProviderKey[] = ['openai', 'google', 'ollama', 'lmstudio'];

interface ProviderModels {
chatModel: string;
Expand All @@ -34,7 +34,8 @@ const getDefaultModels = (provider: ProviderKey): ProviderModels => {
google: 'gemini-1.5-flash',
grok: 'grok-3-mini', // Updated to use grok-3-mini as default
openrouter: 'openai/gpt-4o-mini',
ollama: 'llama3:8b'
ollama: 'llama3:8b',
lmstudio: 'llama-3.2-1b-instruct'
};

const embeddingDefaults: Record<ProviderKey, string> = {
Expand All @@ -43,7 +44,8 @@ const getDefaultModels = (provider: ProviderKey): ProviderModels => {
google: 'text-embedding-004',
grok: 'text-embedding-3-small', // Fallback to OpenAI
openrouter: 'text-embedding-3-small',
ollama: 'nomic-embed-text'
ollama: 'nomic-embed-text',
lmstudio: 'text-embedding-nomic-embed-text'
};

return {
Expand Down Expand Up @@ -71,7 +73,7 @@ const loadProviderModels = (): ProviderModelMap => {
}

// Return defaults for all providers if nothing saved
const providers: ProviderKey[] = ['openai', 'google', 'openrouter', 'ollama', 'anthropic', 'grok'];
const providers: ProviderKey[] = ['openai', 'google', 'openrouter', 'ollama', 'anthropic', 'grok', 'lmstudio'];
const defaultModels: ProviderModelMap = {} as ProviderModelMap;

providers.forEach(provider => {
Expand All @@ -89,6 +91,7 @@ const colorStyles: Record<ProviderKey, string> = {
ollama: 'border-purple-500 bg-purple-500/10',
anthropic: 'border-orange-500 bg-orange-500/10',
grok: 'border-yellow-500 bg-yellow-500/10',
lmstudio: 'border-indigo-500 bg-indigo-500/10',
};

const providerWarningAlertStyle = 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-300';
Expand All @@ -98,14 +101,15 @@ const providerMissingAlertStyle = providerErrorAlertStyle;
const providerDisplayNames: Record<ProviderKey, string> = {
openai: 'OpenAI',
google: 'Google',
lmstudio: 'LM-Studio',
openrouter: 'OpenRouter',
ollama: 'Ollama',
anthropic: 'Anthropic',
grok: 'Grok',
};

const isProviderKey = (value: unknown): value is ProviderKey =>
typeof value === 'string' && ['openai', 'google', 'openrouter', 'ollama', 'anthropic', 'grok'].includes(value);
typeof value === 'string' && ['openai', 'google', 'openrouter', 'ollama', 'anthropic', 'grok', 'lmstudio'].includes(value);

// Default base URL for Ollama instances when not explicitly configured
const DEFAULT_OLLAMA_URL = 'http://host.docker.internal:11434/v1';
Expand Down Expand Up @@ -970,6 +974,10 @@ const manualTestConnection = async (

return 'missing';
}
case 'lmstudio':
// LM-Studio runs locally and doesn't require an API key
// Always return 'configured' since it's a local service
return 'configured';
case 'anthropic':
const hasAnthropicKey = hasApiCredential('ANTHROPIC_API_KEY');
const anthropicConnected = providerConnectionStatus['anthropic']?.connected || false;
Expand Down Expand Up @@ -1013,6 +1021,8 @@ const manualTestConnection = async (
providerAlertMessage = 'Local Ollama service detected. Click "Test Connection" to confirm model availability.';
providerAlertClassName = providerWarningAlertStyle;
}
} else if (activeProviderKey === 'lmstudio') {
// LM-Studio is a local service, no API key needed - no alert
} else if (activeProviderKey && selectedProviderStatus === 'missing') {
const providerName = providerDisplayNames[activeProviderKey] ?? activeProviderKey;
providerAlertMessage = `${providerName} API key is not configured. Add it in Settings > API Keys.`;
Expand Down Expand Up @@ -1291,13 +1301,14 @@ const manualTestConnection = async (
Select {activeSelection === 'chat' ? 'Chat' : 'Embedding'} Provider
</label>
<div className={`grid gap-3 mb-4 ${
activeSelection === 'chat' ? 'grid-cols-6' : 'grid-cols-3'
activeSelection === 'chat' ? 'grid-cols-7' : 'grid-cols-4'
}`}>
{[
{ key: 'openai', name: 'OpenAI', logo: '/img/OpenAI.png', color: 'green' },
{ key: 'google', name: 'Google', logo: '/img/google-logo.svg', color: 'blue' },
{ key: 'openrouter', name: 'OpenRouter', logo: '/img/OpenRouter.png', color: 'cyan' },
{ key: 'ollama', name: 'Ollama', logo: '/img/Ollama.png', color: 'purple' },
{ key: 'lmstudio', name: 'LM-Studio', logo: '/img/LM-Studio.svg', color: 'indigo' },
{ key: 'anthropic', name: 'Anthropic', logo: '/img/claude-logo.svg', color: 'orange' },
{ key: 'grok', name: 'Grok', logo: '/img/Grok.png', color: 'yellow' }
]
Expand Down
3 changes: 3 additions & 0 deletions archon-ui-main/src/services/credentialsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface RagSettings {
LLM_INSTANCE_NAME?: string;
OLLAMA_EMBEDDING_URL?: string;
OLLAMA_EMBEDDING_INSTANCE_NAME?: string;
LMSTUDIO_BASE_URL?: string;
EMBEDDING_MODEL?: string;
EMBEDDING_PROVIDER?: string;
// Crawling Performance Settings
Expand Down Expand Up @@ -201,6 +202,7 @@ class CredentialsService {
LLM_INSTANCE_NAME: "",
OLLAMA_EMBEDDING_URL: "",
OLLAMA_EMBEDDING_INSTANCE_NAME: "",
LMSTUDIO_BASE_URL: "",
EMBEDDING_PROVIDER: "openai",
EMBEDDING_MODEL: "",
// Crawling Performance Settings defaults
Expand Down Expand Up @@ -233,6 +235,7 @@ class CredentialsService {
"LLM_INSTANCE_NAME",
"OLLAMA_EMBEDDING_URL",
"OLLAMA_EMBEDDING_INSTANCE_NAME",
"LMSTUDIO_BASE_URL",
"EMBEDDING_PROVIDER",
"EMBEDDING_MODEL",
"CRAWL_WAIT_STRATEGY",
Expand Down
95 changes: 86 additions & 9 deletions docs/docs/mcp-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,37 @@ AGENTS_BASE_URL=http://archon-agents:8052
# Authentication
MCP_SERVICE_KEY=your-service-key

# Transport Configuration
ARCHON_MCP_ENABLE_STREAMABLE_HTTP=true # Enable Streamable HTTP at /mcp (recommended)
ARCHON_MCP_ENABLE_SSE=true # Enable SSE at /sse (legacy support)
# Note: At least one transport must be enabled

# Unified Logging Configuration (Optional)
LOGFIRE_ENABLED=false # true=Logfire logging, false=standard logging
LOGFIRE_TOKEN=your-logfire-token # Only required when LOGFIRE_ENABLED=true
```

### Transport Configuration

Control which transport protocols the MCP server exposes:

- **`ARCHON_MCP_ENABLE_STREAMABLE_HTTP`** (default: `true`): Enable modern Streamable HTTP transport at `/mcp`
- **`ARCHON_MCP_ENABLE_SSE`** (default: `true`): Enable legacy SSE transport at `/sse`

Both transports are enabled by default for maximum compatibility. You can disable one if you only need a single transport:

```bash
# Streamable HTTP only (recommended for new deployments)
ARCHON_MCP_ENABLE_STREAMABLE_HTTP=true
ARCHON_MCP_ENABLE_SSE=false

# SSE only (legacy systems)
ARCHON_MCP_ENABLE_STREAMABLE_HTTP=false
ARCHON_MCP_ENABLE_SSE=true
```

> **Important**: At least one transport must be enabled. The server will fail to start if both are disabled.

### Docker Service

```yaml
Expand All @@ -95,34 +121,85 @@ archon-mcp:
command: ["python", "-m", "src.mcp.mcp_server"]
```

## Transport Support

Archon MCP server supports **dual transport** for maximum compatibility:

### Available Transports

| Transport | Endpoint | Status | Recommended |
|-----------|----------|--------|-------------|
| **Streamable HTTP** | `/mcp` | Active | βœ… Modern MCP clients |
| **SSE** | `/sse` | Legacy | ⚠️ Backward compatibility only |

### When to Use Each Transport

**Use Streamable HTTP (`/mcp`) when:**
- Using modern MCP clients (Claude Code, Claude Desktop, latest Cursor)
- Starting new integrations
- You want the latest MCP protocol features (2025-03-26 spec)

**Use SSE (`/sse`) when:**
- Maintaining existing integrations that rely on SSE
- Using older MCP clients that don't support Streamable HTTP
- You need backward compatibility with legacy systems

> **Migration Note**: SSE transport is maintained for backward compatibility but is considered legacy. New integrations should use Streamable HTTP (`/mcp`).

## Client Configuration

Archon MCP server uses **SSE (Server-Sent Events) transport only**.
### Claude Code (Recommended)

### Cursor IDE
**Streamable HTTP (Recommended):**
```bash
claude mcp add archon http://localhost:8051/mcp
```

**SSE (Legacy):**
```bash
claude mcp add --transport sse archon http://localhost:8051/sse
```

Add to MCP settings:
### Cursor IDE

**Streamable HTTP (Recommended):**
```json
{
"mcpServers": {
"archon": {
"uri": "http://localhost:8051/sse"
"url": "http://localhost:8051/mcp",
"transport": "streamable-http"
}
}
}
```

### Claude Code

```bash
claude mcp add --transport sse archon http://localhost:8051/sse
**SSE (Legacy):**
```json
{
"mcpServers": {
"archon": {
"uri": "http://localhost:8051/sse"
}
}
}
```

### Windsurf IDE

Add to settings:
**Streamable HTTP (Recommended):**
```json
{
"mcp.servers": {
"archon": {
"url": "http://localhost:8051/mcp",
"transport": "streamable-http"
}
}
}
```

**SSE (Legacy):**
```json
{
"mcp.servers": {
Expand Down
50 changes: 49 additions & 1 deletion python/src/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,64 @@

import asyncio
import logging
import os
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Generic, TypeVar

from openai import AsyncOpenAI
from pydantic import BaseModel
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

logger = logging.getLogger(__name__)


def _prepare_model_for_agent(model_string: str) -> str | OpenAIChatModel:
"""
Prepare model string for PydanticAI Agent, handling LM-Studio provider.

PydanticAI doesn't have built-in support for "lmstudio:" prefix, but since
LM-Studio uses OpenAI-compatible API, we can create a custom OpenAI model
with the LM-Studio base URL.

Args:
model_string: Model string in format "provider:model-name" (e.g., "lmstudio:llama-3.2-1b-instruct")

Returns:
Either the original model string (for built-in providers) or a configured OpenAIChatModel
"""
if not model_string or ":" not in model_string:
return model_string

provider, model_name = model_string.split(":", 1)

# Handle LM-Studio as a special case
if provider.lower() == "lmstudio":
# Get LM-Studio base URL from environment or use default
base_url = os.getenv("LM_STUDIO_BASE_URL", "http://host.docker.internal:1234/v1")

logger.info(f"Creating LM-Studio model with base_url: {base_url}, model: {model_name}")

# Create custom OpenAI-compatible client for LM-Studio
client = AsyncOpenAI(
api_key="lm-studio", # LM-Studio doesn't require a real API key
base_url=base_url
)

# Create OpenAIChatModel with custom provider
return OpenAIChatModel(
model_name,
provider=OpenAIProvider(openai_client=client)
)

# For all other providers (openai, anthropic, google, etc.), return as-is
# PydanticAI has built-in support for these
return model_string


@dataclass
class ArchonDependencies:
"""Base dependencies for all Archon agents."""
Expand Down Expand Up @@ -158,7 +205,8 @@ def __init__(
enable_rate_limiting: bool = True,
**agent_kwargs,
):
self.model = model
# Prepare model for PydanticAI (handles LM-Studio and other custom providers)
self.model = _prepare_model_for_agent(model)
self.name = name or self.__class__.__name__
self.retries = retries
self.enable_rate_limiting = enable_rate_limiting
Expand Down
Loading