@sbissoli/mcp-surface 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +31 -0
- package/dist/cliente.d.ts +175 -0
- package/dist/cliente.d.ts.map +1 -0
- package/dist/cliente.js +164 -0
- package/dist/cliente.js.map +1 -0
- package/package.json +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,43 @@ seguinte — cada servidor faz bump explícito).
|
|
|
8
8
|
e, pela regra da trava, obrigaria cada servidor a subir de versão sem ter mudado nada.
|
|
9
9
|
Mudança de normalização é major (ou minor em 0.x), com nota de migração.
|
|
10
10
|
|
|
11
|
+
## [0.2.1] — 2026-10-04
|
|
12
|
+
|
|
13
|
+
A normalização NÃO muda. Patch: a API de `/cliente` é a mesma; os controles negativos
|
|
14
|
+
ficam mais fortes, e os servidores herdam isso pelo caret `^0.2.0`.
|
|
15
|
+
|
|
16
|
+
### Corrigido
|
|
17
|
+
|
|
18
|
+
- **A troca de tipo cobria UM campo só.** `quebrasDoSchema` trocava o tipo do primeiro
|
|
19
|
+
obrigatório tipado. No ilo-mcp-server esse campo era um objeto (`dataflow`), e os
|
|
20
|
+
escalares ficavam sem prova, o que obrigou a escrever `rows_count` como quebra extra à
|
|
21
|
+
mão. Agora a troca vale para CADA obrigatório com `type` declarado.
|
|
22
|
+
- **O campo anulável não era trocado.** `type: ["string", "null"]` (lista) não gerava
|
|
23
|
+
quebra, e é justamente a classe do defeito que o circuito existe para pegar. O valor
|
|
24
|
+
errado agora é o primeiro de texto, número, booleano, lista e objeto que nenhum dos tipos
|
|
25
|
+
declarados aceita.
|
|
26
|
+
- **As quebras rodam em paralelo.** Com uma troca por obrigatório, em série,
|
|
27
|
+
`loinc_details` do medical-terminologies-mcp passava dos 5 s do vitest sob a carga da
|
|
28
|
+
suíte. Cada quebra já tinha servidor e conexão próprios. A ordem dos vereditos não muda,
|
|
29
|
+
e a armadilha continua sendo o último.
|
|
30
|
+
|
|
31
|
+
Conferido contra os sete servidores com o build local antes de publicar: todos verdes.
|
|
32
|
+
|
|
33
|
+
## [0.2.0] — 2026-10-04
|
|
34
|
+
|
|
35
|
+
A normalização NÃO muda: todo `surface.lock.json` continua conferindo. Minor porque a
|
|
36
|
+
superfície publicada do pacote cresce (subpath novo).
|
|
37
|
+
|
|
38
|
+
### Adicionado
|
|
39
|
+
|
|
40
|
+
- **`@sbissoli/mcp-surface/cliente`**: o teste com forma de cliente, que nasceu no
|
|
41
|
+
ilo-mcp-server 1.3.0 (ideia de leitor, https://dev.to/arhancanli/comment/3g4i4).
|
|
42
|
+
`conectarComoCliente` (transporte em memória com JSON no fio), `chamarComoCliente`
|
|
43
|
+
(`tools/list` antes do `tools/call`, lança se o `Client` reprovar ou vier `isError`),
|
|
44
|
+
`controlesNegativos` (quebras derivadas do `outputSchema` listado, mais a armadilha do
|
|
45
|
+
`tools/list` ausente) e `quebrasDoSchema`. `@modelcontextprotocol/client` entra como peer
|
|
46
|
+
OPCIONAL; a raiz do pacote continua sem o `Client`, que o Worker não pode carregar.
|
|
47
|
+
|
|
11
48
|
## [0.1.1] — 2026-10-02
|
|
12
49
|
|
|
13
50
|
Achados da adoção nos irmãos, no mesmo dia. A normalização NÃO muda: todo
|
package/README.md
CHANGED
|
@@ -70,6 +70,37 @@ travar superfície nova sob a versão antiga.
|
|
|
70
70
|
Fluxo de quem muda a superfície: `npm version <nível> --no-git-tag-version` →
|
|
71
71
|
`npm run surface:lock` → commitar o lock junto.
|
|
72
72
|
|
|
73
|
+
## Teste com forma de cliente (`@sbissoli/mcp-surface/cliente`)
|
|
74
|
+
|
|
75
|
+
O servidor interrogado pelo `Client` do SDK, que reprova o resultado de `tools/call`
|
|
76
|
+
contra o `outputSchema` **listado**. Assim o teste falha como a sessão do usuário falharia,
|
|
77
|
+
sem um validador escolhido por nós. É subpath à parte porque o `Client` compila schemas
|
|
78
|
+
com Ajv (`new Function`), que o Worker proíbe: só se importa em teste Node, e o
|
|
79
|
+
`@modelcontextprotocol/client` é peer opcional.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { chamarComoCliente, conectarComoCliente, controlesNegativos } from "@sbissoli/mcp-surface/cliente";
|
|
83
|
+
|
|
84
|
+
const client = await conectarComoCliente(buildServer(env));
|
|
85
|
+
const r = await chamarComoCliente(client, "minha_tool", { x: 1 }); // lança se o Client reprovar ou se vier isError
|
|
86
|
+
|
|
87
|
+
const vs = await controlesNegativos(() => buildServer(env), "minha_tool", { x: 1 });
|
|
88
|
+
for (const v of vs) expect(v.obtido, `${v.descricao}: ${v.mensagem ?? ""}`).toBe(v.esperado);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `conectarComoCliente` passa toda mensagem do servidor por JSON antes de entregá-la: o
|
|
92
|
+
transporte em memória não serializa, e chave `undefined` só some no fio.
|
|
93
|
+
- `chamarComoCliente` faz `tools/list` antes do primeiro `tools/call` da conexão. Sem
|
|
94
|
+
isso, o `Client` (2.0 a 2.2) devolve o resultado sem validar.
|
|
95
|
+
- `controlesNegativos` adultera o resultado entre servidor e cliente, com quebras
|
|
96
|
+
**derivadas do schema listado**: `structuredContent` ausente, cada obrigatório ausente e
|
|
97
|
+
cada obrigatório com `type` declarado recebendo um valor que nenhum tipo dele aceita (o
|
|
98
|
+
anulável `["string", "null"]` recebe `0`). As quebras rodam em paralelo, cada uma com
|
|
99
|
+
servidor próprio. Cada quebra tem de fazer a chamada falhar. O
|
|
100
|
+
último veredito é a armadilha (sem `tools/list`, a quebra passa calada); se o SDK mudar,
|
|
101
|
+
ele acusa. Quebras do próprio servidor, como campo a mais onde o schema fecha o objeto,
|
|
102
|
+
entram pelo 4º argumento.
|
|
103
|
+
|
|
73
104
|
## Observações
|
|
74
105
|
|
|
75
106
|
- Uma atualização do SDK que mexa nas `capabilities` também acende a trava — de propósito:
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teste com forma de cliente: o servidor interrogado pelo `Client` do SDK, que
|
|
3
|
+
* reprova o resultado de `tools/call` contra o `outputSchema` LISTADO — o teste
|
|
4
|
+
* falha como a sessão do usuário falharia, sem validador escolhido por nós.
|
|
5
|
+
*
|
|
6
|
+
* Ideia de leitor (https://dev.to/arhancanli/comment/3g4i4); o circuito nasceu
|
|
7
|
+
* no ilo-mcp-server 1.3.0 e mora aqui para os sete servidores o usarem igual.
|
|
8
|
+
*
|
|
9
|
+
* Subpath próprio (`@sbissoli/mcp-surface/cliente`), fora da raiz do pacote: o
|
|
10
|
+
* `Client` compila os schemas com Ajv (`new Function`), que o runtime da
|
|
11
|
+
* Cloudflare proíbe. Importar daqui é coisa de teste em Node, nunca de Worker.
|
|
12
|
+
*
|
|
13
|
+
* Duas armadilhas que este módulo fecha, medidas no ilo:
|
|
14
|
+
* - o `Client` só valida contra o schema que tem em cache do `tools/list` —
|
|
15
|
+
* sem `listTools` antes, `callTool` devolve o que recebeu sem validar;
|
|
16
|
+
* - o `InMemoryTransport` não serializa: chave `undefined` sobrevive em memória
|
|
17
|
+
* e some no fio (`JSON.stringify`). Aqui toda mensagem do servidor passa por
|
|
18
|
+
* JSON antes de chegar ao cliente, como passaria pela rede.
|
|
19
|
+
*/
|
|
20
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
21
|
+
import type { ServidorConectavel } from "./memoria.js";
|
|
22
|
+
/** Resultado de `tools/call` como viaja no fio (o que uma quebra recebe). */
|
|
23
|
+
export type ResultadoNoFio = {
|
|
24
|
+
content?: unknown;
|
|
25
|
+
structuredContent?: Record<string, unknown>;
|
|
26
|
+
} & Record<string, unknown>;
|
|
27
|
+
export interface OpcoesConexao {
|
|
28
|
+
/** Mexe no resultado de `tools/call` entre servidor e cliente (controle negativo). */
|
|
29
|
+
adulterar?: (r: ResultadoNoFio) => void;
|
|
30
|
+
}
|
|
31
|
+
export declare function conectarComoCliente(server: ServidorConectavel, opcoes?: OpcoesConexao): Promise<Client>;
|
|
32
|
+
/**
|
|
33
|
+
* Chamada que TEM de dar certo, no percurso do cliente. Lança quando o `Client`
|
|
34
|
+
* reprova o resultado e também quando a tool responde `isError` — o servidor do
|
|
35
|
+
* SDK v2 valida a própria saída e, se ela não obedece, devolve o erro em
|
|
36
|
+
* `isError` ("Output validation error"), não em exceção. Para afirmar uma
|
|
37
|
+
* recusa de propósito (parâmetro inválido), use `client.callTool` direto.
|
|
38
|
+
*/
|
|
39
|
+
export declare function chamarComoCliente(client: Client, nome: string, args?: Record<string, unknown>): Promise<{
|
|
40
|
+
[x: string]: unknown;
|
|
41
|
+
_meta?: {
|
|
42
|
+
[x: string]: unknown;
|
|
43
|
+
"io.modelcontextprotocol/serverInfo"?: {
|
|
44
|
+
version: string;
|
|
45
|
+
websiteUrl?: string | undefined;
|
|
46
|
+
description?: string | undefined;
|
|
47
|
+
icons?: {
|
|
48
|
+
src: string;
|
|
49
|
+
mimeType?: string | undefined;
|
|
50
|
+
sizes?: string[] | undefined;
|
|
51
|
+
theme?: "dark" | "light" | undefined;
|
|
52
|
+
}[] | undefined;
|
|
53
|
+
name: string;
|
|
54
|
+
title?: string | undefined;
|
|
55
|
+
} | undefined;
|
|
56
|
+
} | undefined;
|
|
57
|
+
content: ({
|
|
58
|
+
type: "text";
|
|
59
|
+
text: string;
|
|
60
|
+
annotations?: {
|
|
61
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
62
|
+
priority?: number | undefined;
|
|
63
|
+
lastModified?: string | undefined;
|
|
64
|
+
} | undefined;
|
|
65
|
+
_meta?: {
|
|
66
|
+
[x: string]: unknown;
|
|
67
|
+
} | undefined;
|
|
68
|
+
} | {
|
|
69
|
+
type: "image";
|
|
70
|
+
data: string;
|
|
71
|
+
mimeType: string;
|
|
72
|
+
annotations?: {
|
|
73
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
74
|
+
priority?: number | undefined;
|
|
75
|
+
lastModified?: string | undefined;
|
|
76
|
+
} | undefined;
|
|
77
|
+
_meta?: {
|
|
78
|
+
[x: string]: unknown;
|
|
79
|
+
} | undefined;
|
|
80
|
+
} | {
|
|
81
|
+
type: "audio";
|
|
82
|
+
data: string;
|
|
83
|
+
mimeType: string;
|
|
84
|
+
annotations?: {
|
|
85
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
86
|
+
priority?: number | undefined;
|
|
87
|
+
lastModified?: string | undefined;
|
|
88
|
+
} | undefined;
|
|
89
|
+
_meta?: {
|
|
90
|
+
[x: string]: unknown;
|
|
91
|
+
} | undefined;
|
|
92
|
+
} | {
|
|
93
|
+
uri: string;
|
|
94
|
+
description?: string | undefined;
|
|
95
|
+
mimeType?: string | undefined;
|
|
96
|
+
size?: number | undefined;
|
|
97
|
+
annotations?: {
|
|
98
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
99
|
+
priority?: number | undefined;
|
|
100
|
+
lastModified?: string | undefined;
|
|
101
|
+
} | undefined;
|
|
102
|
+
_meta?: {
|
|
103
|
+
[x: string]: unknown;
|
|
104
|
+
} | undefined;
|
|
105
|
+
icons?: {
|
|
106
|
+
src: string;
|
|
107
|
+
mimeType?: string | undefined;
|
|
108
|
+
sizes?: string[] | undefined;
|
|
109
|
+
theme?: "dark" | "light" | undefined;
|
|
110
|
+
}[] | undefined;
|
|
111
|
+
name: string;
|
|
112
|
+
title?: string | undefined;
|
|
113
|
+
type: "resource_link";
|
|
114
|
+
} | {
|
|
115
|
+
type: "resource";
|
|
116
|
+
resource: {
|
|
117
|
+
uri: string;
|
|
118
|
+
mimeType?: string | undefined;
|
|
119
|
+
_meta?: {
|
|
120
|
+
[x: string]: unknown;
|
|
121
|
+
} | undefined;
|
|
122
|
+
text: string;
|
|
123
|
+
} | {
|
|
124
|
+
uri: string;
|
|
125
|
+
mimeType?: string | undefined;
|
|
126
|
+
_meta?: {
|
|
127
|
+
[x: string]: unknown;
|
|
128
|
+
} | undefined;
|
|
129
|
+
blob: string;
|
|
130
|
+
};
|
|
131
|
+
annotations?: {
|
|
132
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
133
|
+
priority?: number | undefined;
|
|
134
|
+
lastModified?: string | undefined;
|
|
135
|
+
} | undefined;
|
|
136
|
+
_meta?: {
|
|
137
|
+
[x: string]: unknown;
|
|
138
|
+
} | undefined;
|
|
139
|
+
})[];
|
|
140
|
+
structuredContent?: unknown;
|
|
141
|
+
isError?: boolean | undefined;
|
|
142
|
+
}>;
|
|
143
|
+
export interface Quebra {
|
|
144
|
+
descricao: string;
|
|
145
|
+
adulterar: (r: ResultadoNoFio) => void;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* As quebras genéricas, DERIVADAS do schema listado (não de nomes escritos à
|
|
149
|
+
* mão): `structuredContent` ausente, cada campo obrigatório ausente, e cada
|
|
150
|
+
* obrigatório com tipo declarado trocado de tipo. Cada um, não o primeiro: até a
|
|
151
|
+
* 0.2.0 só o primeiro tipado era trocado, e no ilo esse caía num objeto
|
|
152
|
+
* (`dataflow`) e deixava os escalares sem prova.
|
|
153
|
+
*/
|
|
154
|
+
export declare function quebrasDoSchema(outputSchema: Record<string, unknown>): Quebra[];
|
|
155
|
+
export interface Veredito {
|
|
156
|
+
descricao: string;
|
|
157
|
+
/** O que o circuito exige: o cliente reprovar a quebra (ou, na armadilha, deixá-la passar). */
|
|
158
|
+
esperado: "reprova" | "passa";
|
|
159
|
+
/** `isError`: a chamada-base falhou como tool — o caso está mal montado. */
|
|
160
|
+
obtido: "reprova" | "passa" | "isError";
|
|
161
|
+
mensagem?: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Controle negativo pelo lado do resultado: o servidor responde certo, o que
|
|
165
|
+
* chega ao cliente está quebrado, e a chamada TEM de falhar. Um servidor novo
|
|
166
|
+
* por quebra (`fabrica`), porque cada conexão leva a sua adulteração.
|
|
167
|
+
*
|
|
168
|
+
* Inclui a armadilha como veredito esperado "passa": sem `tools/list` antes, a
|
|
169
|
+
* primeira quebra passa calada. Se um dia o SDK passar a validar sem a lista,
|
|
170
|
+
* esse veredito acusa — e a regra do `listTools` pode ser revista.
|
|
171
|
+
*
|
|
172
|
+
* O teste afirma `obtido === esperado` em cada veredito; a `descricao` diz qual.
|
|
173
|
+
*/
|
|
174
|
+
export declare function controlesNegativos(fabrica: () => ServidorConectavel | Promise<ServidorConectavel>, nome: string, args?: Record<string, unknown>, extras?: Quebra[]): Promise<Veredito[]>;
|
|
175
|
+
//# sourceMappingURL=cliente.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cliente.d.ts","sourceRoot":"","sources":["../src/cliente.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;AAGtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,6EAA6E;AAC7E,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1H,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;CACzC;AAID,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAYjH;AAWD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAOvG;AAED,MAAM,WAAW,MAAM;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;CACxC;AAsBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAqB/E;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,QAAQ,EAAE,SAAS,GAAG,OAAO,CAAC;IAC9B,4EAA4E;IAC5E,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,EAC/D,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EAClC,MAAM,GAAE,MAAM,EAAO,GACpB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAuCrB"}
|
package/dist/cliente.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teste com forma de cliente: o servidor interrogado pelo `Client` do SDK, que
|
|
3
|
+
* reprova o resultado de `tools/call` contra o `outputSchema` LISTADO — o teste
|
|
4
|
+
* falha como a sessão do usuário falharia, sem validador escolhido por nós.
|
|
5
|
+
*
|
|
6
|
+
* Ideia de leitor (https://dev.to/arhancanli/comment/3g4i4); o circuito nasceu
|
|
7
|
+
* no ilo-mcp-server 1.3.0 e mora aqui para os sete servidores o usarem igual.
|
|
8
|
+
*
|
|
9
|
+
* Subpath próprio (`@sbissoli/mcp-surface/cliente`), fora da raiz do pacote: o
|
|
10
|
+
* `Client` compila os schemas com Ajv (`new Function`), que o runtime da
|
|
11
|
+
* Cloudflare proíbe. Importar daqui é coisa de teste em Node, nunca de Worker.
|
|
12
|
+
*
|
|
13
|
+
* Duas armadilhas que este módulo fecha, medidas no ilo:
|
|
14
|
+
* - o `Client` só valida contra o schema que tem em cache do `tools/list` —
|
|
15
|
+
* sem `listTools` antes, `callTool` devolve o que recebeu sem validar;
|
|
16
|
+
* - o `InMemoryTransport` não serializa: chave `undefined` sobrevive em memória
|
|
17
|
+
* e some no fio (`JSON.stringify`). Aqui toda mensagem do servidor passa por
|
|
18
|
+
* JSON antes de chegar ao cliente, como passaria pela rede.
|
|
19
|
+
*/
|
|
20
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
21
|
+
import { InMemoryTransport } from "@modelcontextprotocol/server";
|
|
22
|
+
const listados = new WeakSet();
|
|
23
|
+
export async function conectarComoCliente(server, opcoes = {}) {
|
|
24
|
+
const [lado, ladoServidor] = InMemoryTransport.createLinkedPair();
|
|
25
|
+
const enviar = ladoServidor.send.bind(ladoServidor);
|
|
26
|
+
ladoServidor.send = async (mensagem, extra) => {
|
|
27
|
+
const noFio = JSON.parse(JSON.stringify(mensagem));
|
|
28
|
+
const r = noFio.result;
|
|
29
|
+
if (opcoes.adulterar && r && "content" in r)
|
|
30
|
+
opcoes.adulterar(r);
|
|
31
|
+
return enviar(noFio, extra);
|
|
32
|
+
};
|
|
33
|
+
const client = new Client({ name: "mcp-surface/cliente", version: "0.0.0" });
|
|
34
|
+
await Promise.all([server.connect(ladoServidor), client.connect(lado)]);
|
|
35
|
+
return client;
|
|
36
|
+
}
|
|
37
|
+
/** O percurso do cliente: `tools/list` (uma vez por conexão) → `tools/call`. */
|
|
38
|
+
async function percorrer(client, nome, args) {
|
|
39
|
+
if (!listados.has(client)) {
|
|
40
|
+
await client.listTools();
|
|
41
|
+
listados.add(client);
|
|
42
|
+
}
|
|
43
|
+
return client.callTool({ name: nome, arguments: args });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Chamada que TEM de dar certo, no percurso do cliente. Lança quando o `Client`
|
|
47
|
+
* reprova o resultado e também quando a tool responde `isError` — o servidor do
|
|
48
|
+
* SDK v2 valida a própria saída e, se ela não obedece, devolve o erro em
|
|
49
|
+
* `isError` ("Output validation error"), não em exceção. Para afirmar uma
|
|
50
|
+
* recusa de propósito (parâmetro inválido), use `client.callTool` direto.
|
|
51
|
+
*/
|
|
52
|
+
export async function chamarComoCliente(client, nome, args = {}) {
|
|
53
|
+
const r = await percorrer(client, nome, args);
|
|
54
|
+
if (r.isError) {
|
|
55
|
+
const texto = Array.isArray(r.content) ? r.content.map(c => ("text" in c ? c.text : "")).join(" ") : "";
|
|
56
|
+
throw new Error(`${nome} respondeu isError: ${texto}`);
|
|
57
|
+
}
|
|
58
|
+
return r;
|
|
59
|
+
}
|
|
60
|
+
/** Candidatos a valor errado, com os tipos JSON Schema que cada um satisfaz. */
|
|
61
|
+
const CANDIDATOS = [
|
|
62
|
+
["valor-de-tipo-errado", ["string"]],
|
|
63
|
+
[0, ["number", "integer"]],
|
|
64
|
+
[true, ["boolean"]],
|
|
65
|
+
[[], ["array"]],
|
|
66
|
+
[{}, ["object"]],
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Um valor que NENHUM dos tipos declarados aceita — `type` simples ou lista
|
|
70
|
+
* (`["string", "null"]`, o campo anulável, que é onde o defeito mora). Sem
|
|
71
|
+
* `type` declarado não há o que trocar: `undefined`.
|
|
72
|
+
*/
|
|
73
|
+
function tipoErrado(tipo) {
|
|
74
|
+
const tipos = typeof tipo === "string" ? [tipo] : Array.isArray(tipo) ? tipo : [];
|
|
75
|
+
if (tipos.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
return CANDIDATOS.find(([, aceitos]) => !aceitos.some(t => tipos.includes(t)))?.[0];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* As quebras genéricas, DERIVADAS do schema listado (não de nomes escritos à
|
|
81
|
+
* mão): `structuredContent` ausente, cada campo obrigatório ausente, e cada
|
|
82
|
+
* obrigatório com tipo declarado trocado de tipo. Cada um, não o primeiro: até a
|
|
83
|
+
* 0.2.0 só o primeiro tipado era trocado, e no ilo esse caía num objeto
|
|
84
|
+
* (`dataflow`) e deixava os escalares sem prova.
|
|
85
|
+
*/
|
|
86
|
+
export function quebrasDoSchema(outputSchema) {
|
|
87
|
+
const obrigatorios = Array.isArray(outputSchema.required) ? outputSchema.required : [];
|
|
88
|
+
const props = (outputSchema.properties ?? {});
|
|
89
|
+
const quebras = [
|
|
90
|
+
{ descricao: "structuredContent ausente", adulterar: r => void delete r.structuredContent },
|
|
91
|
+
...obrigatorios.map(campo => ({
|
|
92
|
+
descricao: `campo obrigatório ausente (${campo})`,
|
|
93
|
+
adulterar: (r) => void delete r.structuredContent?.[campo],
|
|
94
|
+
})),
|
|
95
|
+
];
|
|
96
|
+
for (const campo of obrigatorios) {
|
|
97
|
+
const errado = tipoErrado(props[campo]?.type);
|
|
98
|
+
if (errado === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
quebras.push({
|
|
101
|
+
descricao: `campo de tipo errado (${campo})`,
|
|
102
|
+
adulterar: r => {
|
|
103
|
+
if (r.structuredContent)
|
|
104
|
+
r.structuredContent[campo] = structuredClone(errado);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return quebras;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Controle negativo pelo lado do resultado: o servidor responde certo, o que
|
|
112
|
+
* chega ao cliente está quebrado, e a chamada TEM de falhar. Um servidor novo
|
|
113
|
+
* por quebra (`fabrica`), porque cada conexão leva a sua adulteração.
|
|
114
|
+
*
|
|
115
|
+
* Inclui a armadilha como veredito esperado "passa": sem `tools/list` antes, a
|
|
116
|
+
* primeira quebra passa calada. Se um dia o SDK passar a validar sem a lista,
|
|
117
|
+
* esse veredito acusa — e a regra do `listTools` pode ser revista.
|
|
118
|
+
*
|
|
119
|
+
* O teste afirma `obtido === esperado` em cada veredito; a `descricao` diz qual.
|
|
120
|
+
*/
|
|
121
|
+
export async function controlesNegativos(fabrica, nome, args = {}, extras = []) {
|
|
122
|
+
const sonda = await conectarComoCliente(await fabrica());
|
|
123
|
+
let schema;
|
|
124
|
+
try {
|
|
125
|
+
const { tools } = await sonda.listTools();
|
|
126
|
+
schema = tools.find(t => t.name === nome)?.outputSchema;
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await sonda.close();
|
|
130
|
+
}
|
|
131
|
+
if (!schema)
|
|
132
|
+
throw new Error(`${nome}: sem outputSchema no tools/list — não há contrato a provar`);
|
|
133
|
+
const quebras = [...quebrasDoSchema(schema), ...extras];
|
|
134
|
+
const rodar = async (q, listar) => {
|
|
135
|
+
const client = await conectarComoCliente(await fabrica(), { adulterar: q.adulterar });
|
|
136
|
+
try {
|
|
137
|
+
const r = listar ? await percorrer(client, nome, args) : await client.callTool({ name: nome, arguments: args });
|
|
138
|
+
// Erro de tool NÃO é reprovação do validador: a chamada-base tem de dar
|
|
139
|
+
// certo, ou o controle não prova nada — veredito que nunca confere.
|
|
140
|
+
if (r.isError)
|
|
141
|
+
return { obtido: "isError", mensagem: JSON.stringify(r.content) };
|
|
142
|
+
return { obtido: "passa" };
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
return { obtido: "reprova", mensagem: e instanceof Error ? e.message : String(e) };
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await client.close();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
// Em paralelo: cada quebra tem servidor e conexão próprios. Desde a 0.2.1 há
|
|
152
|
+
// uma troca de tipo por obrigatório, e em série o medical (`loinc_details`,
|
|
153
|
+
// muitos obrigatórios) passava dos 5 s do vitest sob a carga da suíte.
|
|
154
|
+
const armadilha = quebras[1] ?? quebras[0];
|
|
155
|
+
return Promise.all([
|
|
156
|
+
...quebras.map(async (q) => ({ descricao: q.descricao, esperado: "reprova", ...(await rodar(q, true)) })),
|
|
157
|
+
rodar(armadilha, false).then(r => ({
|
|
158
|
+
descricao: `a armadilha: sem tools/list antes, "${armadilha.descricao}" passa calada`,
|
|
159
|
+
esperado: "passa",
|
|
160
|
+
...r,
|
|
161
|
+
})),
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=cliente.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cliente.js","sourceRoot":"","sources":["../src/cliente.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sbissoli/mcp-surface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Impressão digital da superfície de um servidor MCP: initialize (instructions + capabilities) + tools/resources/prompts + quem responde sem token, travados ao lado da versão — mudou sem subir a versão = build vermelho e deploy recusado; mais a conferência do endpoint no ar e o replay das versões publicadas",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./cliente": {
|
|
22
|
+
"types": "./dist/cliente.d.ts",
|
|
23
|
+
"default": "./dist/cliente.js"
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"bin": {
|
|
@@ -33,9 +37,16 @@
|
|
|
33
37
|
"test": "vitest run"
|
|
34
38
|
},
|
|
35
39
|
"peerDependencies": {
|
|
40
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
36
41
|
"@modelcontextprotocol/server": "^2.0.0"
|
|
37
42
|
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@modelcontextprotocol/client": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
38
48
|
"devDependencies": {
|
|
49
|
+
"@modelcontextprotocol/client": "^2.1.0",
|
|
39
50
|
"@modelcontextprotocol/server": "^2.1.0"
|
|
40
51
|
},
|
|
41
52
|
"keywords": [
|