MCP TypeScript Implementation

Uma implementação em TypeScript do Model Context Protocol para o Personal

Documentação

MCP-PIF

Um runtime de cálculo lambda nativo em JSON com avaliação metacircular, projetado como um servidor MCP (Model Context Protocol). Permite que modelos de linguagem evoluam ferramentas dinamicamente por meio de metaprogramação.

Início Rápido

# Build the project
cabal build

# Enable debug mode for detailed evaluation tracing
MCP_DEBUG=1 cabal run mcp-pif
# Debug output (to stderr) shows:
# - Each evaluation step
# - Environment keys at each step
# - Closure creation and application
# - Tool code lookups

Exemplo Básico

// Create a tool
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "evolve",
    "arguments": {
      "name": "square",
      "description": "Squares a number",
      "code": {"lam": "x", "body": {"mul": [{"var": "x"}, {"var": "x"}]}}
    }
  }
}

// Use the tool
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "run",
    "arguments": {
      "tool": "square",
      "input": 7
    }
  }
}
// Returns: 49

// Get help
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "help",
    "arguments": {"category": "lists"}
  }
}
// Returns: Documentation for list primitives

Conceitos Principais

O Problema

Para computação pura, modelos de linguagem precisam de ferramentas computacionais, mas os sistemas existentes fornecem APIs fixas ou execução de código irrestrita. No contexto de metaprogramação e automodificação, nenhum dos dois é ideal. Este programa apresenta uma estrutura de meio-termo para computação segura, inspecionável e evolutiva.

No sentido ideal, o MCP-PIF pode ser pensado como uma interface de computador genérica e metamórfica, onde o modelo de linguagem atua como função executiva. Aqui, fornecemos um vocabulário simples de primitivas de cálculo lambda para acesso apenas à computação pura.

A Solução

O MCP-PIF fornece um cálculo lambda com três primitivas de metaprogramação:

  • quote - Tratar código como dado (impedir avaliação)
  • eval - Executar código citado dinamicamente
  • code_of - Inspecionar o código-fonte de qualquer ferramenta

Isso cria um sistema metacircular onde ferramentas podem analisar e transformar outras ferramentas, mantendo execução determinística e limitada por combustível. É homoicônico, porém restrito.

Vale notar que quote e eval não são perfeitamente simétricos:

-- eval cleans the environment:
let cleanEnv = M.filterWithKey (\k _ -> not $ k `elem` ["__tool_name", "__self"]) env

Isso impede que o código avaliado herde o contexto errado da ferramenta. Quando eval executa código citado:

  • Variáveis do usuário SÃO preservadas (escopo léxico mantido)
  • Variáveis do sistema são limpas (__tool_name e __self removidas para evitar confusão de contexto da ferramenta)
  • Códigos de ferramentas permanecem disponíveis (para introspecção via code_of)
  • Contador __eval_depth é adicionado (profundidade máxima: 100, evita loops infinitos de eval)

O Horizonte de Eventos

Como este framework é construído em MCP, ele faz concessões significativas em pureza. Especificamente, atualizar o código do servidor não faz parte do loop de metaprogramação. Isso significa que a lista de primitivas, estratégias de parsing e os próprios processos de evolução e avaliação não são modificáveis em tempo de execução.

Existem dois tipos de ferramentas:

  • Ferramentas MCP: Por exemplo, criação via evolve (com efeitos, muta o registro)
  • Ferramentas evoluídas: Execução de ferramentas via run (puras, funcionais)

Ferramentas evoluídas podem interagir entre si, mas não podem evoluir novas ferramentas sem acesso ao registro de ferramentas no nível do protocolo. Essa implementação "simulacro" impede a automodificação ilimitada ao isolar exatamente os invariantes que permitem a metaprogramação rica.

Referência da Linguagem

Primitivas Principais

CategoriaPrimitivaSintaxe JSONDescrição
LambdaVariável{"var": "x"}Referência a variável
Função{"lam": "x", "body": ...}Abstração lambda
Aplicação{"app": {"func": ..., "arg": ...}}Aplicação de função
AritméticaAdição{"add": [a, b]}Adição
Subtração{"sub": [a, b]}Subtração
Multiplicação{"mul": [a, b]}Multiplicação
Divisão{"div": [a, b]}Divisão (erro em 0)
Módulo{"mod": [a, b]}Módulo (erro em 0)
ComparaçãoIgual{"eq": [a, b]}Teste de igualdade
Menor que{"lt": [a, b]}Menor que
Menor ou igual{"lte": [a, b]}Menor ou igual
Maior que{"gt": [a, b]}Maior que
Maior ou igual{"gte": [a, b]}Maior ou igual
LógicaE{"and": [a, b]}E lógico (curto-circuito)
Ou{"or": [a, b]}OU lógico (curto-circuito)
Não{"not": a}NÃO lógico
ControleSe{"if": {"cond": c, "then": t, "else": e}}Condicional
Continuar{"continue": {"input": x}}Passo recursivo
ListasNil{"nil": true}Lista vazia
Cons{"cons": {"head": ..., "tail": ...}}Construção de lista
Fold{"fold": [func, init, list]}Redutor universal (ver GUIDE.md para detalhes do parâmetro de par)
ParesPar{"pair": [a, b]}Construção de par
Primeiro{"fst": pair}Obter primeiro elemento
Segundo{"snd": pair}Obter segundo elemento
MetaQuote{"quote": term}Impedir avaliação
Eval{"eval": quoted}Executar código citado
Código de{"code_of": "tool_name"}Obter código-fonte da ferramenta
Self{"self": true}Referência de fechamento atual (apenas ferramentas)

Literais

  • Números: 42, -17, 3.14 → Inteiros (flutuantes arredondados)
  • Booleanos: true, false
  • Strings: "hello", "world"
  • Arrays: [1, 2, 3] → Convertidos para listas cons
  • Nulo: null → Valor unitário

Normalização de Entrada

A ferramenta run normaliza automaticamente as entradas para tornar o uso via CLI mais ergonômico:

  • Números como string: "42" → 42
  • Booleanos como string: "true" → true, "false" → false
  • Strings JSON: "{\"x\": 5}" → Analisadas como objeto JSON
  • Listas: Arrays JSON são convertidos para listas cons

Isso permite formatos de entrada flexíveis, mantendo a segurança de tipos durante a avaliação.

Referência Rápida

Para padrões e exemplos detalhados, consulte o Guia do Usuário.

Conceitos-Chave para Lembrar

  1. Referências de Ferramentas: Ferramentas podem ser referenciadas como strings ("square"), lambdas inline ou via code_of
  2. Escopo do Eval: Variáveis do usuário são preservadas, variáveis do sistema são limpas, códigos de ferramentas permanecem disponíveis
  3. Assinatura do Fold: A função fold recebe um único par (accumulator, item), não dois parâmetros
  4. Continuações: Funcionam apenas com ferramentas registradas, cada passo é uma ida e volta MCP
  5. Autorreferência: {"self": true} funciona apenas dentro de ferramentas registradas (criadas via evolve), não em lambdas inline passadas para run
  6. Horizonte de Eventos: Ferramentas não podem criar outras ferramentas de dentro do cálculo lambda

Referência Rápida de Recursão

Precisa recursar? Use esta árvore de decisão:

Can you structure it with an accumulator?
├─ Yes → Use `continue` (works for any depth)
│         Pattern: take pair [state, accumulator]
│         Base case: return accumulator
│         Recursive: compute new accumulator, continue with [new_state, new_acc]
│
└─ No, need result immediately?
   ├─ Small input (n < 20) → Use `self`
   └─ Large input → Redesign with accumulator or use fold

Exemplos:

  • Fatorial → continue com padrão de acumulador
  • Soma → continue com padrão de acumulador
  • Fibonacci (n pequeno) → self
  • Recursão mútua Par/Ímpar → eval + code_of

Padrões e Exemplos

Fatorial Recursivo

Ferramentas podem usar recursão baseada em continuação para execução passo a passo. Como continue pausa a avaliação e retorna o controle à camada MCP, você deve usar um padrão de acumulador onde a computação acontece durante a recursão, não depois:

{
  "name": "evolve",
  "arguments": {
    "name": "factorial",
    "description": "Computes factorial using continuation with accumulator",
    "code": {
      "lam": "n_acc",
      "body": {
        "if": {
          "cond": {"lte": [{"fst": {"var": "n_acc"}}, 1]},
          "then": {"snd": {"var": "n_acc"}},
          "else": {
            "continue": {
              "input": {
                "pair": [
                  {"sub": [{"fst": {"var": "n_acc"}}, 1]},
                  {"mul": [{"fst": {"var": "n_acc"}}, {"snd": {"var": "n_acc"}}]}
                ]
              }
            }
          }
        }
      }
    }
  }
}

Uso:

{
  "name": "run",
  "arguments": {
    "code": "factorial",
    "input": {"pair": [5, 1]}
  }
}

O programa retornará uma resposta estruturada:

{
  "type": "continuation",
  "message": "Recursive step needed. Call run again with:",
  "tool": "factorial_acc",
  "next_input": {
    "pair": [4, 5]
  },
  "step": 1
}

Isso é renderizado para o cliente como uma representação Haskell:

Object (fromList [("message",String "Recursive step needed. Call run again with:"),("next_input",Object (fromList [("pair",Array [Number 4.0,Number 5.0])])),("step",Number 1.0),("tool",String "factorial_acc"),("type",String "continuation")])

Importante: A ferramenta recebe um par [n, accumulator] como entrada. Comece com [5, 1] para calcular 5!. Cada passo de continuação multiplica o acumulador pelo n atual e depois decrementa n.

Por que esse padrão?

A primitiva continue não retorna um valor com o qual você possa computar—ela retorna um marcador de continuação. Toda a computação deve acontecer antes de chamar continue, armazenada no acumulador. O padrão é:

  • Entrada: [n, acc] onde acc contém o resultado parcial
  • Caso base: Quando n ≤ 1, retorne o acumulador
  • Caso recursivo: Calcule o novo acumulador (n * acc), continue com [n-1, new_acc]

Alternativa: Recursão direta com self (limitada por combustível):

{
  "name": "evolve",
  "arguments": {
    "name": "factorial_self",
    "description": "Simple factorial using self (small n only)",
    "code": {
      "lam": "n",
      "body": {
        "if": {
          "cond": {"lte": [{"var": "n"}, 1]},
          "then": 1,
          "else": {
            "mul": [
              {"var": "n"},
              {"app": {"func": {"self": true}, "arg": {"sub": [{"var": "n"}, 1]}}}
            ]
          }
        }
      }
    }
  }
}

Isso funciona para entradas pequenas, mas atingirá o limite de combustível (10.000 passos) por volta de n=20.

Map de Ordem Superior via Fold

{
  "name": "map",
  "description": "Maps a function over a list",
  "code": {
    "lam": "f",
    "body": {
      "lam": "list",
      "body": {
        "fold": [
          {"lam": "acc_item", "body": {
            "cons": {
              "head": {"app": {"func": {"var": "f"}, "arg": {"snd": {"var": "acc_item"}}}},
              "tail": {"fst": {"var": "acc_item"}}
            }
          }},
          {"nil": true},
          {"var": "list"}
        ]
      }
    }
  }
}

Metaprogramação: Análise de Código

{
  "name": "count_operations",
  "description": "Counts arithmetic operations in a tool",
  "code": {
    "lam": "tool_name",
    "body": {
      "eval": {
        "quote": {
          "analyze": [{"code_of": {"var": "tool_name"}}]
        }
      }
    }
  }
}

A primitiva code_of retorna o código-fonte de uma ferramenta como dados citados, permitindo análise e transformação de programas.

Arquitetura

Pipeline

JSON Input → Parser → Term → Evaluator → RuntimeValue → Encoder → JSON Output
         validation  syntax    execution     values     serialization

Módulos

MóduloPropósito
Main.hsPonto de entrada, loop JSON-RPC
Server.hsProtocolo MCP, roteamento de requisições
Core/Parser.hsValidação JSON → Termo
Core/Evaluator.hsExecução de termos com combustível
Core/Encoder.hsRuntimeValue → JSON
Core/Types.hsDefinições de tipos principais
Core/Syntax.hsADT de termos
Tools/Registry.hsArmazenamento de ferramentas

Garantias de Segurança

  • Terminação baseada em combustível: Toda avaliação tem passos finitos (padrão: 10.000)
  • Avaliação pura: Sem E/S ou efeitos no cálculo lambda
  • Execução validada: Apenas termos estruturalmente válidos podem ser executados
  • Registro imutável: Ferramentas não podem modificar umas às outras durante a execução

Integração MCP

O MCP-PIF implementa o Model Context Protocol para descoberta e execução de ferramentas:

Ferramentas do Sistema

  • evolve - Criar novas ferramentas (armazena no registro)
  • run - Executar ferramentas ou expressões lambda inline
  • list - Mostrar todas as ferramentas registradas
  • help - Exibir documentação para primitivas e ferramentas do sistema

Fluxo do Protocolo

  1. Cliente envia requisição JSON-RPC para stdin
  2. Servidor analisa e roteia para o manipulador apropriado
  3. Para execução de ferramentas:
    • Analisar JSON de entrada → Termo
    • Injetar códigos de ferramentas no ambiente
    • Avaliar com limite de combustível
    • Codificar resultado → JSON
  4. Resposta enviada para stdout

Conectando um Cliente MCP

# Example using Python MCP SDK
import mcp

async with mcp.Client() as client:
    await client.connect(stdio_transport("cabal run mcp-pif"))

    # Create a tool
    await client.call_tool("evolve", {
        "name": "double",
        "description": "Doubles a number",
        "code": {"mul": [{"var": "x"}, 2]}
    })

    # Use it
    result = await client.call_tool("run", {
        "tool": "double",
        "input": 21
    })
    print(result)  # 42

Direções Futuras

O design atual do MCP-PIF mantém uma fronteira clara entre computação pura e operações com efeitos. Várias extensões foram consideradas que expandiriam essas fronteiras de maneiras interessantes:

Funções Analíticas

O sistema atual é principalmente sintético - usando primitivas para compor novas funções. Uma extensão natural seriam capacidades analíticas:

  • Validação: Análise estática da estrutura de termos sem avaliação
  • Normalização: Redução de termos a formas canônicas
  • Verificação de Equivalência: Provar que dois termos computam a mesma função
  • Inferência de Tipos: Derivar tipos para termos lambda
  • Análise de Complexidade: Estimar requisitos de combustível
  • Análise de Capacidades: Decompor ferramentas por suas primitivas

Essas funções analíticas operariam em código-como-dado (termos citados) e poderiam permitir padrões poderosos de metaprogramação. No entanto, exigem design cuidadoso para manter a simplicidade do cálculo central enquanto fornecem garantias significativas.

Primitivas com Efeitos

Outra direção envolve a introdução controlada de efeitos:

Interação com o Sistema:

  • Primitivas de E/S de arquivo (readFile, writeFile)
  • Controle de processos (exec, env)
  • Operações de rede (fetch, serve)

Desafios de Design:

  • Como manter fronteiras de pureza?
  • Os efeitos devem ser monádicos, algébricos ou baseados em continuação?
  • Como lidar com erros e gerenciamento de recursos?
  • Qual modelo de segurança para controle de capacidades?

Uma abordagem poderia ser segurança baseada em capacidades: ferramentas poderiam declarar capacidades necessárias (acesso a arquivos, rede, etc.) no momento da criação, com a camada MCP aplicando controle de acesso. Isso é consistente com a ideia de que ferramentas evoluídas devem carregar prova de sua própria validade.

Auto-hospedagem e Bootstrapping

O objetivo metacircular final: implementar o avaliador do MCP-PIF no próprio MCP-PIF. Isso exigiria:

  • Primitivas para manipulação de JSON
  • Construções de correspondência de padrões
  • Representação eficiente de ambientes
  • Gerenciamento de combustível no nível meta

Um PIF auto-hospedado poderia permitir a evolução em tempo de execução da própria estratégia de avaliação - um sistema verdadeiramente reflexivo.

Extensões de Protocolo

A fronteira MCP poderia suportar operações adicionais no nível do protocolo:

  • Versionamento de ferramentas: Rastrear a evolução de ferramentas ao longo do tempo
  • Composição de ferramentas: Combinadores no nível do protocolo para fusão de ferramentas
  • Registro distribuído: Compartilhar ferramentas entre servidores MCP
  • Certificados de prova: Anexar provas de correção às ferramentas Essas extensões mantêm o princípio do horizonte de eventos enquanto enriquecem as capacidades da camada de protocolo.

O princípio de design fundamental para qualquer extensão: preservar a simplicidade e a previsibilidade que tornam o PIF um substrato confiável para a computação de modelos de linguagem.

Licença

MIT