nyc311-mcp 1.0.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,41 @@
1
+ # nyc311-mcp
2
+
3
+ Explore public NYC 311 service-request data for civic research, neighborhood-level trend analysis, and city-service questions.
4
+
5
+ ## Tools
6
+
7
+ - `search_requests`: search bounded request metadata by complaint type, borough, agency, and dates.
8
+ - `count_requests`: aggregate request totals by complaint type with the same filters.
9
+
10
+ The server intentionally excludes street addresses, latitude, longitude, and other precise location fields. NYC 311 records are reports of requests, not verified findings, and may contain reporting delays or duplicates. The public Socrata API requires no key for these read-only queries.
11
+
12
+ ## Run
13
+
14
+ ```bash
15
+ npm install
16
+ npm run build
17
+ node dist/index.js
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ npm install
24
+ npm run build
25
+ node dist/index.js
26
+ ```
27
+
28
+ The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
29
+
30
+ ## Tools at a glance
31
+
32
+ - `search_requests`: Search recent public NYC 311 requests using non-location-identifying fields. Addresses and coordinates are intentionally excluded.
33
+ - `count_requests`: Count NYC 311 requests grouped by complaint type with optional date, borough, and agency filters.
34
+
35
+ ## Limits and privacy
36
+
37
+ This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. No credentials are required unless the project explicitly says otherwise.
38
+
39
+ ## Try it
40
+
41
+ After building, connect the server through your MCP client. The repository root also contains `smoke-test.mjs` for projects covered by the shared harness. A typical tool call starts with `search_requests`.
package/dist/api.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare class Nyc311Error extends Error {
2
+ }
3
+ export declare function searchRequests(complaintType?: string, borough?: string, agency?: string, start?: string, end?: string, limit?: number): Promise<unknown>;
4
+ export declare function countRequests(complaintType?: string, borough?: string, agency?: string, start?: string, end?: string, limit?: number): Promise<unknown>;
5
+ export declare function format(value: unknown): string;
package/dist/api.js ADDED
@@ -0,0 +1,64 @@
1
+ const BASE = "https://data.cityofnewyork.us/resource/erm2-nwe9.json";
2
+ const HEADERS = { "User-Agent": "mrfentmen-nyc311-mcp/1.0" };
3
+ export class Nyc311Error extends Error {
4
+ }
5
+ function quote(value) {
6
+ return `'${value.replaceAll("'", "''")}'`;
7
+ }
8
+ function validateDates(start, end) {
9
+ const parse = (value) => {
10
+ const date = new Date(`${value}T00:00:00Z`);
11
+ if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value)
12
+ throw new Nyc311Error(`Invalid date: ${value}`);
13
+ return date;
14
+ };
15
+ if (start && end && parse(start) >= parse(end))
16
+ throw new Nyc311Error("start must be earlier than end");
17
+ if (start)
18
+ parse(start);
19
+ if (end)
20
+ parse(end);
21
+ }
22
+ function filters(complaintType, borough, agency, start, end) {
23
+ validateDates(start, end);
24
+ const result = [];
25
+ if (complaintType)
26
+ result.push(`complaint_type = ${quote(complaintType)}`);
27
+ if (borough)
28
+ result.push(`borough = ${quote(borough.toUpperCase())}`);
29
+ if (agency)
30
+ result.push(`agency = ${quote(agency.toUpperCase())}`);
31
+ if (start)
32
+ result.push(`created_date >= ${quote(start)}`);
33
+ if (end)
34
+ result.push(`created_date < ${quote(end)}`);
35
+ return result;
36
+ }
37
+ async function query(params) {
38
+ const url = new URL(BASE);
39
+ Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
40
+ const response = await fetch(url, { headers: HEADERS, signal: AbortSignal.timeout(30000) });
41
+ if (!response.ok)
42
+ throw new Nyc311Error(`NYC 311 error ${response.status}`);
43
+ return response.json();
44
+ }
45
+ export function searchRequests(complaintType, borough, agency, start, end, limit = 20) {
46
+ const where = filters(complaintType, borough, agency, start, end);
47
+ return query({
48
+ $select: "created_date,closed_date,agency,complaint_type,descriptor,borough,status",
49
+ $order: "created_date DESC",
50
+ $limit: String(limit),
51
+ ...(where.length ? { $where: where.join(" AND ") } : {}),
52
+ });
53
+ }
54
+ export function countRequests(complaintType, borough, agency, start, end, limit = 100) {
55
+ const where = filters(complaintType, borough, agency, start, end);
56
+ return query({
57
+ $select: "complaint_type,count(*) as total",
58
+ $group: "complaint_type",
59
+ $order: "total DESC",
60
+ $limit: String(limit),
61
+ ...(where.length ? { $where: where.join(" AND ") } : {}),
62
+ });
63
+ }
64
+ export function format(value) { return JSON.stringify(value, null, 2).slice(0, 16000); }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+ async function main() {
4
+ const server = createServer();
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
7
+ console.error("MCP server running on stdio");
8
+ }
9
+ main().catch((err) => {
10
+ console.error("Fatal error:", err);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,51 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { countRequests, format, searchRequests } from "./api.js";
4
+ const text = (value) => ({ content: [{ type: "text", text: value }] });
5
+ const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
7
+ const errorText = (error) => text(`Error: ${error instanceof Error ? error.message : String(error)}`);
8
+ const date = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}/).optional();
9
+ export function createServer() {
10
+ const server = new McpServer({ name: "nyc311-mcp", version: "1.0.0" });
11
+ const filters = {
12
+ complaintType: z.string().max(120).optional(),
13
+ borough: z.enum(["MANHATTAN", "BRONX", "BROOKLYN", "QUEENS", "STATEN ISLAND"]).optional(),
14
+ agency: z.string().max(20).optional(),
15
+ start: date,
16
+ end: date,
17
+ };
18
+ server.registerTool("search_requests", {
19
+ title: "Search requests",
20
+ description: "Search recent public NYC 311 requests using non-location-identifying fields. Addresses and coordinates are intentionally excluded.",
21
+ inputSchema: z.object({
22
+ ...filters,
23
+ limit: z.number().int().min(1).max(50).default(20),
24
+ }),
25
+ annotations: READ_ONLY,
26
+ }, async ({ complaintType, borough, agency, start, end, limit }) => {
27
+ try {
28
+ return text(format(await searchRequests(complaintType, borough, agency, start, end, limit)));
29
+ }
30
+ catch (error) {
31
+ return errorText(error);
32
+ }
33
+ });
34
+ server.registerTool("count_requests", {
35
+ title: "Count requests",
36
+ description: "Count NYC 311 requests grouped by complaint type with optional date, borough, and agency filters.",
37
+ inputSchema: z.object({
38
+ ...filters,
39
+ limit: z.number().int().min(1).max(100).default(50),
40
+ }),
41
+ annotations: READ_ONLY,
42
+ }, async ({ complaintType, borough, agency, start, end, limit }) => {
43
+ try {
44
+ return text(format(await countRequests(complaintType, borough, agency, start, end, limit)));
45
+ }
46
+ catch (error) {
47
+ return errorText(error);
48
+ }
49
+ });
50
+ return server;
51
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "nyc311-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Use this MCP server to NYC 311 public service request search and aggregate civic data. Tools include search requests, count requests",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/nyc311-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mrfentmen/nyc311-mcp.git"
10
+ },
11
+ "bin": {
12
+ "nyc311-mcp": "./dist/index.js"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "files": [
16
+ "dist",
17
+ "server.json",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "start": "node dist/index.js",
23
+ "dev": "npm run build && node dist/index.js"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "public-data",
28
+ "nyc311"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.0.4",
33
+ "zod": "^3.23.8"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "typescript": "^5.6.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ }
42
+ }
package/server.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.mrfentmen/nyc311-mcp",
4
+ "description": "Use this MCP server to NYC 311 public service request search and aggregate civic data. Tools...",
5
+ "repository": {
6
+ "url": "https://github.com/mrfentmen/nyc311-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "nyc311-mcp",
14
+ "version": "1.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }