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 dinamicamentecode_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_namee__selfremovidas 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
| Categoria | Primitiva | Sintaxe JSON | Descrição |
|---|---|---|---|
| Lambda | Variá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ética | Adiçã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ção | Igual | {"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ógica | E | {"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 | |
| Controle | Se | {"if": {"cond": c, "then": t, "else": e}} | Condicional |
| Continuar | {"continue": {"input": x}} | Passo recursivo | |
| Listas | Nil | {"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) | |
| Pares | Par | {"pair": [a, b]} | Construção de par |
| Primeiro | {"fst": pair} | Obter primeiro elemento | |
| Segundo | {"snd": pair} | Obter segundo elemento | |
| Meta | Quote | {"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
- Referências de Ferramentas: Ferramentas podem ser referenciadas como strings (
"square"), lambdas inline ou viacode_of - 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
- Assinatura do Fold: A função fold recebe um único par
(accumulator, item), não dois parâmetros - Continuações: Funcionam apenas com ferramentas registradas, cada passo é uma ida e volta MCP
- Autorreferência:
{"self": true}funciona apenas dentro de ferramentas registradas (criadas viaevolve), não em lambdas inline passadas pararun - 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 →
continuecom padrão de acumulador - Soma →
continuecom 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]ondeaccconté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ódulo | Propósito |
|---|---|
| Main.hs | Ponto de entrada, loop JSON-RPC |
| Server.hs | Protocolo MCP, roteamento de requisições |
| Core/Parser.hs | Validação JSON → Termo |
| Core/Evaluator.hs | Execução de termos com combustível |
| Core/Encoder.hs | RuntimeValue → JSON |
| Core/Types.hs | Definições de tipos principais |
| Core/Syntax.hs | ADT de termos |
| Tools/Registry.hs | Armazenamento 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 inlinelist- Mostrar todas as ferramentas registradashelp- Exibir documentação para primitivas e ferramentas do sistema
Fluxo do Protocolo
- Cliente envia requisição JSON-RPC para stdin
- Servidor analisa e roteia para o manipulador apropriado
- 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
- 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