senado-br-mcp 3.2.1 → 3.3.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/README.md CHANGED
@@ -94,6 +94,18 @@ list/corpus tools: without D1 they fall back to a live scrape of the ~5 REST hig
94
94
  `meta.fonte` / `possivelDesatualizacao`; the detail tools (`obter_*`) are identical. Logs go to
95
95
  **stderr** — stdout carries only the JSON-RPC protocol stream.
96
96
 
97
+ ## Agent Skill (optional)
98
+
99
+ This repo bundles a Claude [Agent Skill](https://platform.claude.com/docs/en/docs/agents-and-tools/agent-skills/overview)
100
+ at [`.claude/skills/senado-br/`](.claude/skills/senado-br/SKILL.md) that teaches Claude **when** to reach for
101
+ this server and **how** to use its 66 tools well — a themed tool map, common question→tool playbooks, the
102
+ provenance contract, and gotchas (dates, the `codigoMateria` bridge, e-Cidadania's open-set listing, pagination).
103
+ It points back to the server's own `senado://catalogo` / `senado://guia` resources rather than duplicating them.
104
+
105
+ Claude Code auto-discovers it when you work in this repo. To use it elsewhere, copy
106
+ `.claude/skills/senado-br/` into your `~/.claude/skills/`, or zip the folder and upload it in claude.ai
107
+ (Settings → Features). The skill assumes the `senado-br` MCP server is connected (hosted or via npx).
108
+
97
109
  ## Architecture
98
110
 
99
111
  - **Runtime:** Cloudflare Workers (ESM)
package/README.pt-BR.md CHANGED
@@ -93,6 +93,19 @@ as ferramentas de lista/corpus do e-Cidadania: sem D1, elas caem no scraping ao
93
93
  REST, sinalizado por `meta.fonte` / `possivelDesatualizacao`; as de detalhe (`obter_*`) são idênticas.
94
94
  Os logs vão para o **stderr** — o stdout carrega apenas o fluxo do protocolo JSON-RPC.
95
95
 
96
+ ## Agent Skill (opcional)
97
+
98
+ Este repositório inclui uma [Agent Skill](https://platform.claude.com/docs/en/docs/agents-and-tools/agent-skills/overview)
99
+ do Claude em [`.claude/skills/senado-br/`](.claude/skills/senado-br/SKILL.md) que ensina o Claude **quando**
100
+ recorrer a este servidor e **como** usar bem as 66 ferramentas — um mapa de ferramentas por tema, playbooks de
101
+ pergunta→ferramenta, o contrato de proveniência e os gotchas (datas, a ponte `codigoMateria`, a listagem
102
+ só-abertas do e-Cidadania, paginação). Ela aponta para os recursos `senado://catalogo` / `senado://guia` do
103
+ próprio servidor em vez de duplicá-los.
104
+
105
+ O Claude Code a descobre automaticamente ao trabalhar neste repositório. Para usá-la em outro lugar, copie
106
+ `.claude/skills/senado-br/` para o seu `~/.claude/skills/`, ou compacte a pasta e faça upload no claude.ai
107
+ (Configurações → Recursos). A skill pressupõe o servidor MCP `senado-br` conectado (hospedado ou via npx).
108
+
96
109
  ## Arquitetura
97
110
 
98
111
  - **Runtime:** Cloudflare Workers (ESM)
@@ -11,6 +11,16 @@
11
11
  */
12
12
  import { UPSTREAM_TIMEOUT_MS } from "../types.js";
13
13
  export const ECIDADANIA_BASE = "https://www12.senado.leg.br/ecidadania";
14
+ /**
15
+ * Error carrying a `retryable` flag, mirroring UpstreamError so the tool layer
16
+ * (`errorFrom` / `ecidadaniaError`) can surface it. e-Cidadania has its own fetch
17
+ * (not `upstreamFetch`), so transient conditions are classified here: 5xx / 429 /
18
+ * timeout / network → retryable; 4xx → not.
19
+ */
20
+ function ecidadaniaFetchError(message, retryable) {
21
+ return Object.assign(new Error(message), { retryable });
22
+ }
23
+ const isTransientStatus = (status) => status >= 500 || status === 429;
14
24
  /** Fetch an HTML page from e-Cidadania with descriptive errors. */
15
25
  export async function fetchPage(path) {
16
26
  const url = `${ECIDADANIA_BASE}${path}`;
@@ -25,19 +35,22 @@ export async function fetchPage(path) {
25
35
  signal: controller.signal,
26
36
  });
27
37
  clearTimeout(timeout);
28
- if (!resp.ok)
29
- throw new Error(`e-Cidadania retornou HTTP ${resp.status} para ${path}`);
38
+ if (!resp.ok) {
39
+ throw ecidadaniaFetchError(`e-Cidadania retornou HTTP ${resp.status} para ${path}`, isTransientStatus(resp.status));
40
+ }
30
41
  const text = await resp.text();
31
42
  if (!text.trim())
32
- throw new Error(`e-Cidadania retornou página vazia para ${path}`);
43
+ throw ecidadaniaFetchError(`e-Cidadania retornou página vazia para ${path}`, true);
33
44
  return text;
34
45
  }
35
46
  catch (e) {
36
47
  clearTimeout(timeout);
37
48
  if (e.name === "AbortError") {
38
- throw new Error(`e-Cidadania: timeout (${UPSTREAM_TIMEOUT_MS / 1000}s) ao acessar ${path}`);
49
+ throw ecidadaniaFetchError(`e-Cidadania: timeout (${UPSTREAM_TIMEOUT_MS / 1000}s) ao acessar ${path}`, true);
39
50
  }
40
- throw e;
51
+ if (e instanceof Error && "retryable" in e)
52
+ throw e; // already classified above
53
+ throw ecidadaniaFetchError(`e-Cidadania: falha de rede ao acessar ${path} (${e.message})`, true);
41
54
  }
42
55
  }
43
56
  /**
@@ -54,14 +67,15 @@ export async function fetchEcidadaniaJson(endpoint) {
54
67
  signal: controller.signal,
55
68
  });
56
69
  clearTimeout(timeout);
57
- if (!resp.ok)
58
- throw new Error(`e-Cidadania REST API retornou HTTP ${resp.status} para ${endpoint}`);
70
+ if (!resp.ok) {
71
+ throw ecidadaniaFetchError(`e-Cidadania REST API retornou HTTP ${resp.status} para ${endpoint}`, isTransientStatus(resp.status));
72
+ }
59
73
  let data;
60
74
  try {
61
75
  data = await resp.json();
62
76
  }
63
77
  catch {
64
- throw new Error(`e-Cidadania REST API retornou JSON inválido para ${endpoint}`);
78
+ throw ecidadaniaFetchError(`e-Cidadania REST API retornou JSON inválido para ${endpoint}`, true);
65
79
  }
66
80
  if (!Array.isArray(data)) {
67
81
  // API returned an object or other non-array — wrap or return empty
@@ -74,9 +88,11 @@ export async function fetchEcidadaniaJson(endpoint) {
74
88
  catch (e) {
75
89
  clearTimeout(timeout);
76
90
  if (e.name === "AbortError") {
77
- throw new Error(`e-Cidadania REST API: timeout (${UPSTREAM_TIMEOUT_MS / 1000}s) ao acessar ${endpoint}`);
91
+ throw ecidadaniaFetchError(`e-Cidadania REST API: timeout (${UPSTREAM_TIMEOUT_MS / 1000}s) ao acessar ${endpoint}`, true);
78
92
  }
79
- throw e;
93
+ if (e instanceof Error && "retryable" in e)
94
+ throw e; // already classified above
95
+ throw ecidadaniaFetchError(`e-Cidadania REST API: falha de rede ao acessar ${endpoint} (${e.message})`, true);
80
96
  }
81
97
  }
82
98
  /** Parse Brazilian number: 1.234.567 -> 1234567 */
@@ -1,10 +1,25 @@
1
1
  /** Shared validation helpers and response builders. */
2
2
  import { incr } from "../metrics.js";
3
3
  import { logger } from "./logger.js";
4
- export function toolError(message, isRetryable = false) {
4
+ /** Actionable next-step guidance, derived from whether retrying can help. */
5
+ function defaultHint(isRetryable) {
6
+ return isRetryable
7
+ ? "Erro transitório na fonte de dados oficial; repita a chamada em alguns segundos."
8
+ : "Erro não recuperável por repetição; verifique os parâmetros (códigos, datas, filtros). Se persistir, a fonte oficial pode estar indisponível.";
9
+ }
10
+ /**
11
+ * Standard error envelope, identical across all tools: `{ error, retryable, hint }`.
12
+ * `hint` is an actionable next step (additive, defaults from `isRetryable`; callers may
13
+ * override). The same payload is also returned as `structuredContent` so clients can parse
14
+ * errors deterministically — symmetric with toolResult() — the permissive outputSchema is
15
+ * skipped on isError results anyway, and a plain object satisfies it regardless.
16
+ */
17
+ export function toolError(message, isRetryable = false, hint) {
5
18
  incr("toolErrors");
19
+ const payload = { error: message, retryable: isRetryable, hint: hint ?? defaultHint(isRetryable) };
6
20
  return {
7
- content: [{ type: "text", text: JSON.stringify({ error: message, retryable: isRetryable }) }],
21
+ content: [{ type: "text", text: JSON.stringify(payload) }],
22
+ structuredContent: payload,
8
23
  isError: true,
9
24
  };
10
25
  }
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Single source of truth for the server version (kept in sync with package.json). */
2
- export const VERSION = "3.2.1";
2
+ export const VERSION = "3.3.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senado-br-mcp",
3
- "version": "3.2.1",
3
+ "version": "3.3.1",
4
4
  "description": "MCP server for Brazilian Senate open data — runs hosted on Cloudflare Workers or locally via npx (stdio)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,12 +29,13 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.26.0",
32
- "agents": "^0.5.0",
33
32
  "zod": "^3.23.0"
34
33
  },
35
34
  "devDependencies": {
36
35
  "@cloudflare/workers-types": "^4.20250214.0",
37
36
  "@types/node": "^25.9.3",
37
+ "agents": "^0.5.0",
38
+ "exceljs": "^4.4.0",
38
39
  "tsx": "^4.19.2",
39
40
  "typescript": "^5.7.0",
40
41
  "vitest": "^4.0.18",