@openar/mcp 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ const API = "https://api.openar.pt";
6
+ async function get(path) {
7
+ const res = await fetch(`${API}${path}`);
8
+ if (!res.ok)
9
+ throw new Error(`API error ${res.status}: ${path}`);
10
+ return res.json();
11
+ }
12
+ function qs(params) {
13
+ const p = new URLSearchParams();
14
+ for (const [k, v] of Object.entries(params)) {
15
+ if (v !== undefined && v !== "")
16
+ p.append(k, String(v));
17
+ }
18
+ const s = p.toString();
19
+ return s ? `?${s}` : "";
20
+ }
21
+ const server = new McpServer({
22
+ name: "openar",
23
+ version: "0.1.0",
24
+ });
25
+ // ── Meta ────────────────────────────────────────────────────────────────────
26
+ server.tool("get_meta", "Get available filter values: legislaturas, grupos parlamentares, tipos de iniciativa", { legislatura: z.string().optional().describe("Filter grupos/tipos to a specific legislatura") }, async ({ legislatura }) => {
27
+ const data = await get(`/meta${qs({ legislatura })}`);
28
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
29
+ });
30
+ // ── Iniciativas ──────────────────────────────────────────────────────────────
31
+ server.tool("list_iniciativas", "List legislative initiatives with optional filters", {
32
+ legislatura: z.string().optional().describe("e.g. XVII, XVI"),
33
+ tipo: z.enum(["R", "P", "J", "D", "S", "A", "I", "C"]).optional()
34
+ .describe("R=Resolução P=Proposta J=Projeto D=Decreto S=Outros A=Apreciação I=Europeia C=Pergunta"),
35
+ estado: z.string().optional().describe("e.g. Aprovado, Rejeitado, Caducado"),
36
+ grupo: z.string().optional().describe("Party abbreviation e.g. PS, PSD, CH"),
37
+ resultado: z.enum(["aprovado", "rejeitado", "pendente"]).optional(),
38
+ q: z.string().optional().describe("Search in title"),
39
+ deputado: z.string().optional().describe("Deputy ID or name (partial match)"),
40
+ page: z.number().int().min(1).default(1).optional(),
41
+ limit: z.number().int().min(1).max(200).default(50).optional(),
42
+ }, async (params) => {
43
+ const data = await get(`/iniciativas${qs(params)}`);
44
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
45
+ });
46
+ server.tool("get_iniciativa", "Get a legislative initiative with full detail: authors, events, votes, publications", { id: z.number().int().describe("Initiative ID (IniId)") }, async ({ id }) => {
47
+ const data = await get(`/iniciativas/${id}`);
48
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
49
+ });
50
+ // ── Deputados ────────────────────────────────────────────────────────────────
51
+ server.tool("list_deputados", "List MPs with optional filters", {
52
+ legislatura: z.string().optional().describe("e.g. XVII"),
53
+ grupo: z.string().optional().describe("Party abbreviation e.g. PS, PSD, BE"),
54
+ q: z.string().optional().describe("Search by name"),
55
+ situacao: z.string().optional().describe("Use 'ativo' for current MPs"),
56
+ page: z.number().int().min(1).default(1).optional(),
57
+ limit: z.number().int().min(1).max(200).default(50).optional(),
58
+ }, async (params) => {
59
+ const data = await get(`/deputados${qs(params)}`);
60
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
61
+ });
62
+ server.tool("get_deputado", "Get an MP with all their mandates and recent initiatives", { id: z.number().int().describe("Deputy ID (DepCadId)") }, async ({ id }) => {
63
+ const data = await get(`/deputados/${id}`);
64
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
65
+ });
66
+ server.tool("get_deputado_atividade", "Get full parliamentary activity for an MP: initiatives, requirements, plenary interventions, committees", {
67
+ id: z.number().int().describe("Deputy ID (DepCadId)"),
68
+ legislatura: z.string().optional().describe("e.g. XVII — omit for full history"),
69
+ }, async ({ id, legislatura }) => {
70
+ const data = await get(`/deputados/${id}/atividade${qs({ legislatura })}`);
71
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
72
+ });
73
+ // ── Votações ─────────────────────────────────────────────────────────────────
74
+ server.tool("list_votacoes", "List plenary votes with optional filters", {
75
+ legislatura: z.string().optional().describe("e.g. XVII"),
76
+ resultado: z.enum(["Aprovado", "Rejeitado"]).optional(),
77
+ unanime: z.boolean().optional(),
78
+ data_inicio: z.string().optional().describe("YYYY-MM-DD"),
79
+ data_fim: z.string().optional().describe("YYYY-MM-DD"),
80
+ page: z.number().int().min(1).default(1).optional(),
81
+ limit: z.number().int().min(1).max(200).default(50).optional(),
82
+ }, async (params) => {
83
+ const data = await get(`/votacoes${qs(params)}`);
84
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
85
+ });
86
+ server.tool("get_votacao", "Get a single vote with party breakdown", {
87
+ id: z.string().describe("Vote ID"),
88
+ iniciativa_id: z.number().int().describe("Initiative ID (required — vote IDs are unique per initiative)"),
89
+ }, async ({ id, iniciativa_id }) => {
90
+ const data = await get(`/votacoes/${id}${qs({ iniciativa_id })}`);
91
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
92
+ });
93
+ // ── Petições ─────────────────────────────────────────────────────────────────
94
+ server.tool("list_peticoes", "List petitions with optional filters", {
95
+ legislatura: z.string().optional().describe("e.g. XVII"),
96
+ situacao: z.string().optional().describe("e.g. Admitida, Arquivada"),
97
+ q: z.string().optional().describe("Search in subject"),
98
+ page: z.number().int().min(1).default(1).optional(),
99
+ limit: z.number().int().min(1).max(200).default(50).optional(),
100
+ }, async (params) => {
101
+ const data = await get(`/peticoes${qs(params)}`);
102
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
103
+ });
104
+ server.tool("get_peticao", "Get a petition with committees, rapporteurs, and documents", { id: z.number().int().describe("Petition ID") }, async ({ id }) => {
105
+ const data = await get(`/peticoes/${id}`);
106
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
107
+ });
108
+ // ── Start ────────────────────────────────────────────────────────────────────
109
+ const transport = new StdioServerTransport();
110
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@openar/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for openAR — Portuguese parliamentary data",
5
+ "type": "module",
6
+ "bin": {
7
+ "openar-mcp": "dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "openar",
18
+ "parlamento",
19
+ "portugal"
20
+ ],
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.29.0",
24
+ "zod": "^4.4.3"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^25.9.1",
28
+ "tsx": "^4.22.3",
29
+ "typescript": "^6.0.3"
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+
6
+ const API = "https://api.openar.pt";
7
+
8
+ async function get<T>(path: string): Promise<T> {
9
+ const res = await fetch(`${API}${path}`);
10
+ if (!res.ok) throw new Error(`API error ${res.status}: ${path}`);
11
+ return res.json() as Promise<T>;
12
+ }
13
+
14
+ function qs(params: Record<string, string | number | boolean | undefined>): string {
15
+ const p = new URLSearchParams();
16
+ for (const [k, v] of Object.entries(params)) {
17
+ if (v !== undefined && v !== "") p.append(k, String(v));
18
+ }
19
+ const s = p.toString();
20
+ return s ? `?${s}` : "";
21
+ }
22
+
23
+ const server = new McpServer({
24
+ name: "openar",
25
+ version: "0.1.0",
26
+ });
27
+
28
+ // ── Meta ────────────────────────────────────────────────────────────────────
29
+
30
+ server.tool(
31
+ "get_meta",
32
+ "Get available filter values: legislaturas, grupos parlamentares, tipos de iniciativa",
33
+ { legislatura: z.string().optional().describe("Filter grupos/tipos to a specific legislatura") },
34
+ async ({ legislatura }) => {
35
+ const data = await get(`/meta${qs({ legislatura })}`);
36
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
37
+ }
38
+ );
39
+
40
+ // ── Iniciativas ──────────────────────────────────────────────────────────────
41
+
42
+ server.tool(
43
+ "list_iniciativas",
44
+ "List legislative initiatives with optional filters",
45
+ {
46
+ legislatura: z.string().optional().describe("e.g. XVII, XVI"),
47
+ tipo: z.enum(["R", "P", "J", "D", "S", "A", "I", "C"]).optional()
48
+ .describe("R=Resolução P=Proposta J=Projeto D=Decreto S=Outros A=Apreciação I=Europeia C=Pergunta"),
49
+ estado: z.string().optional().describe("e.g. Aprovado, Rejeitado, Caducado"),
50
+ grupo: z.string().optional().describe("Party abbreviation e.g. PS, PSD, CH"),
51
+ resultado: z.enum(["aprovado", "rejeitado", "pendente"]).optional(),
52
+ q: z.string().optional().describe("Search in title"),
53
+ deputado: z.string().optional().describe("Deputy ID or name (partial match)"),
54
+ page: z.number().int().min(1).default(1).optional(),
55
+ limit: z.number().int().min(1).max(200).default(50).optional(),
56
+ },
57
+ async (params) => {
58
+ const data = await get(`/iniciativas${qs(params)}`);
59
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
60
+ }
61
+ );
62
+
63
+ server.tool(
64
+ "get_iniciativa",
65
+ "Get a legislative initiative with full detail: authors, events, votes, publications",
66
+ { id: z.number().int().describe("Initiative ID (IniId)") },
67
+ async ({ id }) => {
68
+ const data = await get(`/iniciativas/${id}`);
69
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
70
+ }
71
+ );
72
+
73
+ // ── Deputados ────────────────────────────────────────────────────────────────
74
+
75
+ server.tool(
76
+ "list_deputados",
77
+ "List MPs with optional filters",
78
+ {
79
+ legislatura: z.string().optional().describe("e.g. XVII"),
80
+ grupo: z.string().optional().describe("Party abbreviation e.g. PS, PSD, BE"),
81
+ q: z.string().optional().describe("Search by name"),
82
+ situacao: z.string().optional().describe("Use 'ativo' for current MPs"),
83
+ page: z.number().int().min(1).default(1).optional(),
84
+ limit: z.number().int().min(1).max(200).default(50).optional(),
85
+ },
86
+ async (params) => {
87
+ const data = await get(`/deputados${qs(params)}`);
88
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
89
+ }
90
+ );
91
+
92
+ server.tool(
93
+ "get_deputado",
94
+ "Get an MP with all their mandates and recent initiatives",
95
+ { id: z.number().int().describe("Deputy ID (DepCadId)") },
96
+ async ({ id }) => {
97
+ const data = await get(`/deputados/${id}`);
98
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
99
+ }
100
+ );
101
+
102
+ server.tool(
103
+ "get_deputado_atividade",
104
+ "Get full parliamentary activity for an MP: initiatives, requirements, plenary interventions, committees",
105
+ {
106
+ id: z.number().int().describe("Deputy ID (DepCadId)"),
107
+ legislatura: z.string().optional().describe("e.g. XVII — omit for full history"),
108
+ },
109
+ async ({ id, legislatura }) => {
110
+ const data = await get(`/deputados/${id}/atividade${qs({ legislatura })}`);
111
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
112
+ }
113
+ );
114
+
115
+ // ── Votações ─────────────────────────────────────────────────────────────────
116
+
117
+ server.tool(
118
+ "list_votacoes",
119
+ "List plenary votes with optional filters",
120
+ {
121
+ legislatura: z.string().optional().describe("e.g. XVII"),
122
+ resultado: z.enum(["Aprovado", "Rejeitado"]).optional(),
123
+ unanime: z.boolean().optional(),
124
+ data_inicio: z.string().optional().describe("YYYY-MM-DD"),
125
+ data_fim: z.string().optional().describe("YYYY-MM-DD"),
126
+ page: z.number().int().min(1).default(1).optional(),
127
+ limit: z.number().int().min(1).max(200).default(50).optional(),
128
+ },
129
+ async (params) => {
130
+ const data = await get(`/votacoes${qs(params)}`);
131
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
132
+ }
133
+ );
134
+
135
+ server.tool(
136
+ "get_votacao",
137
+ "Get a single vote with party breakdown",
138
+ {
139
+ id: z.string().describe("Vote ID"),
140
+ iniciativa_id: z.number().int().describe("Initiative ID (required — vote IDs are unique per initiative)"),
141
+ },
142
+ async ({ id, iniciativa_id }) => {
143
+ const data = await get(`/votacoes/${id}${qs({ iniciativa_id })}`);
144
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
145
+ }
146
+ );
147
+
148
+ // ── Petições ─────────────────────────────────────────────────────────────────
149
+
150
+ server.tool(
151
+ "list_peticoes",
152
+ "List petitions with optional filters",
153
+ {
154
+ legislatura: z.string().optional().describe("e.g. XVII"),
155
+ situacao: z.string().optional().describe("e.g. Admitida, Arquivada"),
156
+ q: z.string().optional().describe("Search in subject"),
157
+ page: z.number().int().min(1).default(1).optional(),
158
+ limit: z.number().int().min(1).max(200).default(50).optional(),
159
+ },
160
+ async (params) => {
161
+ const data = await get(`/peticoes${qs(params)}`);
162
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
163
+ }
164
+ );
165
+
166
+ server.tool(
167
+ "get_peticao",
168
+ "Get a petition with committees, rapporteurs, and documents",
169
+ { id: z.number().int().describe("Petition ID") },
170
+ async ({ id }) => {
171
+ const data = await get(`/peticoes/${id}`);
172
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
173
+ }
174
+ );
175
+
176
+ // ── Start ────────────────────────────────────────────────────────────────────
177
+
178
+ const transport = new StdioServerTransport();
179
+ await server.connect(transport);
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }