@onegovpolicy/claude-desktop-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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # OneGov Claude Desktop MCP Connector
2
+
3
+ This package is the small local MCP process used by Claude Desktop. It does not connect to the
4
+ OneGov database or require a source checkout. It fetches the MCP tool catalog from the OneGov
5
+ website and proxies tool calls to `/api/mcp/invoke` with an account-scoped API key.
6
+
7
+ Node.js 20 or newer is required.
8
+
9
+ Use the config generated in OneGov Settings -> Integrations -> Claude Desktop MCP.
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "onegov": {
15
+ "command": "npx",
16
+ "args": ["-y", "@onegovpolicy/claude-desktop-mcp"],
17
+ "env": {
18
+ "ONEGOV_API_BASE_URL": "https://your-onegov-website.example",
19
+ "ONEGOV_MCP_API_KEY": "og_mcp_replace_with_generated_key"
20
+ }
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ The key is stored in plaintext in Claude Desktop's config file until it is removed. Revoke unused
27
+ keys from the OneGov integrations page.
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+
7
+ const VERSION = '0.1.0';
8
+
9
+ const jsonResult = (value) => ({
10
+ content: [
11
+ {
12
+ type: 'text',
13
+ text: JSON.stringify(value, null, 2),
14
+ },
15
+ ],
16
+ });
17
+
18
+ function isRecord(value) {
19
+ return typeof value === 'object' && value !== null;
20
+ }
21
+
22
+ function getString(value) {
23
+ return typeof value === 'string' ? value : undefined;
24
+ }
25
+
26
+ function getApiBaseUrl() {
27
+ const value = process.env.ONEGOV_API_BASE_URL?.trim();
28
+ if (!value) {
29
+ throw new Error('Set ONEGOV_API_BASE_URL to your OneGov website URL.');
30
+ }
31
+ return value.replace(/\/$/, '');
32
+ }
33
+
34
+ function getApiKey() {
35
+ const value = process.env.ONEGOV_MCP_API_KEY?.trim();
36
+ if (!value) {
37
+ throw new Error('Set ONEGOV_MCP_API_KEY to an active account-scoped MCP API key.');
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function getSchemaType(schema) {
43
+ const type = schema.type;
44
+ if (typeof type === 'string') {
45
+ return type;
46
+ }
47
+ if (Array.isArray(type)) {
48
+ return type.find((item) => typeof item === 'string' && item !== 'null');
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ function getRequiredNames(schema) {
54
+ const required = schema.required;
55
+ const names = new Set();
56
+ if (!Array.isArray(required)) {
57
+ return names;
58
+ }
59
+ for (const item of required) {
60
+ if (typeof item === 'string') {
61
+ names.add(item);
62
+ }
63
+ }
64
+ return names;
65
+ }
66
+
67
+ function getEnumValues(schema) {
68
+ const values = schema.enum;
69
+ if (!Array.isArray(values)) {
70
+ return [];
71
+ }
72
+ return values.filter((value) => typeof value === 'string');
73
+ }
74
+
75
+ function withDescription(schema, source) {
76
+ const description = getString(source.description);
77
+ return description ? schema.describe(description) : schema;
78
+ }
79
+
80
+ function jsonSchemaPropertyToZod(schema) {
81
+ if (!isRecord(schema)) {
82
+ return z.unknown();
83
+ }
84
+
85
+ const enumValues = getEnumValues(schema);
86
+ if (enumValues.length > 0) {
87
+ return withDescription(
88
+ z.string().refine((value) => enumValues.includes(value), {
89
+ message: `Expected one of: ${enumValues.join(', ')}`,
90
+ }),
91
+ schema,
92
+ );
93
+ }
94
+
95
+ const type = getSchemaType(schema);
96
+ if (type === 'string') {
97
+ return withDescription(z.string(), schema);
98
+ }
99
+ if (type === 'number' || type === 'integer') {
100
+ const numberSchema = type === 'integer' ? z.number().int() : z.number();
101
+ return withDescription(numberSchema, schema);
102
+ }
103
+ if (type === 'boolean') {
104
+ return withDescription(z.boolean(), schema);
105
+ }
106
+ if (type === 'array') {
107
+ const items = isRecord(schema.items) ? jsonSchemaPropertyToZod(schema.items) : z.unknown();
108
+ return withDescription(z.array(items), schema);
109
+ }
110
+ if (type === 'object') {
111
+ return withDescription(buildZodObjectFromJsonSchema(schema), schema);
112
+ }
113
+ return withDescription(z.unknown(), schema);
114
+ }
115
+
116
+ function buildZodObjectFromJsonSchema(parameters) {
117
+ if (!isRecord(parameters)) {
118
+ return z.object({});
119
+ }
120
+
121
+ const properties = isRecord(parameters.properties) ? parameters.properties : {};
122
+ const required = getRequiredNames(parameters);
123
+ const shape = {};
124
+ for (const [name, property] of Object.entries(properties)) {
125
+ const propertySchema = jsonSchemaPropertyToZod(property);
126
+ shape[name] = required.has(name) ? propertySchema : propertySchema.optional();
127
+ }
128
+
129
+ return parameters.additionalProperties === true ? z.object(shape).passthrough() : z.object(shape);
130
+ }
131
+
132
+ function isMcpToolDefinition(value) {
133
+ if (!isRecord(value)) {
134
+ return false;
135
+ }
136
+ if (value.type !== 'function' || !isRecord(value.function)) {
137
+ return false;
138
+ }
139
+ return (
140
+ typeof value.function.name === 'string' &&
141
+ typeof value.function.description === 'string' &&
142
+ 'parameters' in value.function
143
+ );
144
+ }
145
+
146
+ function parseToolDefinitionsPayload(payload) {
147
+ if (!isRecord(payload) || !Array.isArray(payload.tools)) {
148
+ throw new Error('OneGov returned an invalid MCP tool catalog.');
149
+ }
150
+
151
+ const tools = [];
152
+ for (const item of payload.tools) {
153
+ if (isMcpToolDefinition(item)) {
154
+ tools.push(item);
155
+ }
156
+ }
157
+ return tools;
158
+ }
159
+
160
+ async function readJsonResponse(response) {
161
+ const responseText = await response.text();
162
+ if (responseText.length === 0) {
163
+ return null;
164
+ }
165
+ try {
166
+ return JSON.parse(responseText);
167
+ } catch {
168
+ return responseText;
169
+ }
170
+ }
171
+
172
+ async function fetchMcpToolDefinitions() {
173
+ const response = await fetch(`${getApiBaseUrl()}/api/mcp/tools`, {
174
+ headers: {
175
+ Authorization: `Bearer ${getApiKey()}`,
176
+ },
177
+ });
178
+ const payload = await readJsonResponse(response);
179
+ if (!response.ok) {
180
+ const message =
181
+ isRecord(payload) && typeof payload.message === 'string'
182
+ ? payload.message
183
+ : `OneGov returned HTTP ${response.status} while loading MCP tools`;
184
+ throw new Error(message);
185
+ }
186
+ return parseToolDefinitionsPayload(payload);
187
+ }
188
+
189
+ async function postToolInvocation(toolName, args) {
190
+ const response = await fetch(`${getApiBaseUrl()}/api/mcp/invoke`, {
191
+ method: 'POST',
192
+ headers: {
193
+ Authorization: `Bearer ${getApiKey()}`,
194
+ 'Content-Type': 'application/json',
195
+ },
196
+ body: JSON.stringify({ toolName, args }),
197
+ });
198
+
199
+ const payload = await readJsonResponse(response);
200
+ if (!response.ok) {
201
+ const message =
202
+ isRecord(payload) && typeof payload.message === 'string'
203
+ ? payload.message
204
+ : `OneGov returned HTTP ${response.status}`;
205
+ throw new Error(message);
206
+ }
207
+
208
+ return isRecord(payload) && 'result' in payload ? payload.result : payload;
209
+ }
210
+
211
+ async function createServer() {
212
+ const toolDefinitions = await fetchMcpToolDefinitions();
213
+ const server = new McpServer({
214
+ name: 'onegov',
215
+ version: VERSION,
216
+ });
217
+
218
+ server.registerTool(
219
+ 'onegov_health',
220
+ {
221
+ title: 'OneGov MCP Health',
222
+ description: 'Checks Claude Desktop MCP configuration and the OneGov website connection.',
223
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
224
+ },
225
+ async () =>
226
+ jsonResult({
227
+ status: 'ok',
228
+ apiBaseUrl: getApiBaseUrl(),
229
+ apiKeyConfigured: Boolean(process.env.ONEGOV_MCP_API_KEY?.trim()),
230
+ exposedToolCount: toolDefinitions.length,
231
+ }),
232
+ );
233
+
234
+ for (const tool of toolDefinitions) {
235
+ const toolName = tool.function.name;
236
+ const inputSchema = buildZodObjectFromJsonSchema(tool.function.parameters);
237
+ server.registerTool(
238
+ toolName,
239
+ {
240
+ title: toolName,
241
+ description: tool.function.description,
242
+ inputSchema,
243
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
244
+ },
245
+ async (args) => {
246
+ const toolArgs = isRecord(args) ? args : {};
247
+ const result = await postToolInvocation(toolName, toolArgs);
248
+ return jsonResult(result);
249
+ },
250
+ );
251
+ }
252
+
253
+ return server;
254
+ }
255
+
256
+ async function main() {
257
+ const server = await createServer();
258
+ const transport = new StdioServerTransport();
259
+ await server.connect(transport);
260
+ }
261
+
262
+ main().catch((error) => {
263
+ const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
264
+ process.stderr.write(`OneGov MCP connector failed to start: ${message}\n`);
265
+ process.exit(1);
266
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@onegovpolicy/claude-desktop-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Claude Desktop MCP connector for OneGov",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "bin": {
8
+ "onegov-claude-desktop-mcp": "bin/onegov-claude-desktop-mcp.mjs"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20.19.0 <21"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.29.0",
22
+ "zod": "^3.24.2"
23
+ }
24
+ }