npm package: @getjavelin/scout
Description: Javelin Scout
Author: Javelin Team
Latest version: 0.0.9

# Javelin Scout

[![npm version](https://badge.fury.io/js/%40getjavelin%2Fscout.svg)](https://badge.fury.io/js/%40getjavelin%2Fscout)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**Real-time monitoring for Model Context Protocol (MCP) communication using eBPF technology with Javelin guardrail integration.**

## Features

- **🔒 Real-time MCP Monitoring**: Monitor Model Context Protocol communication using eBPF
- **🛡️ Javelin Guardrail Integration**: Real-time security validation and analysis
- **📊 Multiple Transport Support**: Stdio, HTTP/HTTPS, and Server-Sent Events
- **⚡ Service Management**: Easy installation and management as a system service
- **🐛 Debugging Tools**: Troubleshoot MCP integrations with detailed message flow
- **📈 Performance Monitoring**: Track message patterns and identify bottlenecks

## Installation

### Prerequisites

- Linux kernel version 5.15 or later
- Root privileges (required for eBPF)
- Node.js 14.0.0 or later

### NPM Package Installation

```bash
# Install globally
sudo npm install -g @getjavelin/scout

```

## Usage

### Configuration

Before running the service, configure the following environment variables:

**JAVELIN_URL** → Base URL of your Javelin Core deployment (typically ends with /v1).  
*To get this: Contact your Javelin administrator or check your Javelin Core deployment documentation.*

**JAVELIN_API_KEY** → Your API Key for authenticating with the Javelin Gateway.  
*To get this: Log into your Javelin Gateway dashboard and generate an API key from the settings section.*

By default, scout will automatically provision an MCP Overwatch application (mcp_overwatch) in your Javelin Gateway when started if it doesn't already exist.


### What happens after starting?

Once you start the service, scout will:

1. **Monitor MCP traffic** in real-time using eBPF technology
2. **Display live logs** showing MCP operations, requests, and responses
3. **Process guardrails** we will process the MCP operations and send data to your Javelin Gateway

### Running as a Service vs Terminal

**Run as Background Service (Recommended)**
```bash
# Install as system service (prompts for configuration)
scout install

# Start the service
scout start

# Service runs in background - terminal can be closed
# Use scout logs to view output
# Use scout stop to stop the service
```

### Service Management

Javelin Scout can be installed and managed as a system service:

```bash
# Install as a system service (prompts for API key and URL)
scout install

# Start the service
scout start

# Show config
scout config show

# Update config (optional)
scout config set

# Stop the service
scout stop

# Restart the service
scout restart

# Check service status
scout status

# View service logs (follow mode)
scout logs

# Show help
scout help
```


## Architecture

Javelin Scout consists of several key components:

- **eBPF Program**: Hooks into kernel functions for low-level monitoring
- **HTTP Session Manager**: Manages HTTP/HTTPS sessions and correlates request/response pairs
- **MCP Protocol Parser**: Validates JSON-RPC 2.0 message format and parses MCP-specific methods
- **Javelin Integration**: Message correlation and guardrail processing
- **Service Management**: NPM package wrapper with Supervisor integration



## K8s Deployment

* Download and update the `javelin-charts` into local

```bash
helm repo add javelin-charts "https://getjavelin.github.io/charts"
helm repo update javelin-charts && helm search repo javelin-charts
```

* Create a `value.yaml` file for the helm deployment

```code
image:
  repository: "ghcr.io/getjavelin/release-javelin-scout"
  pullPolicy: Always
  # Overrides the image tag with a specific version.
  tag: "latest"

imagePullSecrets:
  - name: javelin-registry-secret

secrets:
  enabled: true
  secretData:
    JAVELIN_URL: 'https://be-domain/v1'
    JAVELIN_API_KEY: '${JAVELIN_API_KEY}'
```

* Deploy the `javelin-scout` to the kubernetes cluster

```bash
kubectl create ns javelin-scout
helm upgrade --install javelin-scout javelin-charts/javelin-scout \
    --namespace javelin-scout \
    -f value.yaml --timeout=10m
```

## Limitations

- **Platform**: Linux only (kernel 5.15+)
- **Buffer Size**: Limited to 16KB per message
- **Javelin Service**: Guardrail processing requires valid Javelin configuration and network connectivity
- **Root Privileges**: Required for eBPF operations

## Example: MCP client that calls DeepWiki

**DeepWikiMCP** is an MCP server that provides tools to analyze GitHub repository structures and documentation. This example shows how to use it as a test client to generate MCP traffic that scout can monitor.

1. Below is a minimal Python client that calls the `read_wiki_structure` tool on the DeepWiki MCP. Save as deepwiki_call_read_structure.py (or a name you prefer).

2. `python deepwiki_call_read_structure.py --repo "getjavelin/ramparts"` Use this command to run the deepwiki client and generate MCP traffic that scout will monitor.

3. 

```python
#!/usr/bin/env python3
"""
- Install python env
- Install httpx
- Install mcp 

Call read_wiki_structure on DeepWiki MCP (minimal, fixed factory)

Usage:
  python3 deepwiki_call_read_structure.py --repo "github.com/getjavelin/ramparts" \
      [--bearer ABC123] [--transport http|sse]

Defaults:
  endpoint: https://mcp.deepwiki.com/mcp
  transport: http (streamable HTTP)
"""
import argparse
import asyncio
import json
import logging
from typing import Any, Dict, Optional

import httpx
from mcp import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client


ENDPOINT_DEFAULT = "https://mcp.deepwiki.com/mcp"
TOOL_NAME = "read_wiki_structure"


def httpx_client_factory_factory(bearer_from_cli: Optional[str] = None, verify_tls: bool = True):
    """
    Returns a factory function matching the signature expected by streamablehttp_client:
      factory(headers=None, timeout=None, auth=None, **kwargs) -> httpx.AsyncClient
    """

    def factory(headers: Optional[Dict[str, str]] = None, timeout: Optional[Any] = None, auth: Optional[Any] = None, **kwargs) -> httpx.AsyncClient:
        client_kwargs: Dict[str, Any] = {
            "follow_redirects": True,
            "verify": verify_tls,
        }

        # honor provided timeout (httpx.Timeout object or numeric); fallback to 30s
        client_kwargs["timeout"] = timeout if timeout is not None else 30.0

        # Merge headers: transport headers (if any) + bearer_from_cli header
        merged_headers: Dict[str, str] = {}
        if headers:
            merged_headers.update(headers)
        if bearer_from_cli:
            # only set Authorization if not already set by transport
            merged_headers.setdefault("Authorization", f"Bearer {bearer_from_cli}")
        if merged_headers:
            client_kwargs["headers"] = merged_headers

        if auth is not None:
            client_kwargs["auth"] = auth

        return httpx.AsyncClient(**client_kwargs)

    return factory


async def call_read_wiki_structure_over_http(endpoint: str, headers: Dict[str, str], factory, repo: str):
    async with streamablehttp_client(endpoint, httpx_client_factory=factory) as (read_stream, write_stream, _):
        async with ClientSession(read_stream, write_stream) as session:
            await session.initialize()
            result = await session.call_tool(TOOL_NAME, {"repoName": repo})
            return result


async def call_read_wiki_structure_over_sse(endpoint: str, repo: str):
    async with sse_client(endpoint) as (read_stream, write_stream):
        async with ClientSession(read_stream, write_stream) as session:
            await session.initialize()
            result = await session.call_tool(TOOL_NAME, {"repo": repo})
            return result


def pretty_print_result(result):
    try:
        # If Pydantic-like model with model_dump()
        print(json.dumps(result.model_dump(), indent=2))  # type: ignore[attr-defined]
    except Exception:
        # Fallback: let json try to serialize, else print repr
        try:
            print(json.dumps(result, default=str, indent=2))
        except Exception:
            print(repr(result))


async def main_async(args):
    endpoint = args.url
    repo = args.repo
    bearer = args.bearer
    transport = args.transport.lower()

    # Prepare factory that accepts (headers, timeout, auth, **kwargs)
    factory = httpx_client_factory_factory(bearer_from_cli=bearer, verify_tls=not args.insecure)

    if transport == "http":
        result = await call_read_wiki_structure_over_http(endpoint, {}, factory, repo)
    elif transport == "sse":
        result = await call_read_wiki_structure_over_sse(endpoint, repo)
    else:
        raise SystemExit("transport must be 'http' or 'sse'")

    pretty_print_result(result)


def parse_args():
    p = argparse.ArgumentParser(description="Call read_wiki_structure on DeepWiki MCP")
    p.add_argument("--url", default=ENDPOINT_DEFAULT, help="MCP endpoint (default: https://mcp.deepwiki.com/mcp)")
    p.add_argument("--repo", required=True, help="Repo identifier (e.g. github.com/org/repo)")
    p.add_argument("--bearer", help="Bearer token for Authorization header")
    p.add_argument("--transport", choices=["http", "sse"], default="http", help="Transport (http or sse)")
    p.add_argument("--insecure", action="store_true", help="Disable TLS verification (useful for self-signed certs)")
    return p.parse_args()


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
    args = parse_args()
    try:
        asyncio.run(main_async(args))
    except Exception as exc:
        logging.exception("Error during MCP call: %s", exc)
        raise
```

---

<div align="center">
Made with ❤️ by the Javelin Team
</div>