Skip to main content
Glama
shivamchhaya

agency-intelligence-mcp

by shivamchhaya
README.md
# agency-intelligence-mcp

A TypeScript MCP server for researching businesses and corporate companies, using the official `@modelcontextprotocol/sdk` on Cloudflare Workers. Plain `fetch` handler, Web Standard Streamable HTTP, JSON responses, and no Node runtime compatibility flag or paid Cloudflare bindings.

## 1. Install

Use Node.js 22+ for development tooling. The deployed code runs on Workers, not Node.js.

```sh
cd /path/to/agency-intelligence-mcp
npm install
cp .dev.vars.example .dev.vars
```

Optional secrets in `.dev.vars`:

```dotenv
JINA_API_KEY=your-jina-api-key
MCP_AUTH_TOKEN=your-long-random-token
```

With an explicit `domain`, the server tries Jina Reader without a key and falls back to direct HTML/text fetch. A Jina key is required for company-name-only discovery. Jina service quotas and pricing are separate from Cloudflare; no LLM API is required.

## 2. Validate and run locally

```sh
npm run check
npm run dev
```

Wrangler serves `http://localhost:8787`. In another terminal:

```sh
curl http://localhost:8787/health

# Leave empty if MCP_AUTH_TOKEN is unset; otherwise use its value.
export MCP_TOKEN=''

curl http://localhost:8787/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"terminal-test","version":"1.0.0"}}}'

curl http://localhost:8787/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

curl http://localhost:8787/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

curl http://localhost:8787/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"audit_company","arguments":{"company_name":"Microsoft","domain":"https://www.microsoft.com","industry":"Technology"}}}'

curl http://localhost:8787/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"generate_application_angle","arguments":{"company_name":"Example Company","role_title":"Operations Manager","candidate_strengths":"process improvement, stakeholder communication, and data analysis"}}}'
```

Alternatively, run the official MCP Inspector:

```sh
npx @modelcontextprotocol/inspector
```

Select **Streamable HTTP**, enter `http://localhost:8787/mcp`, and configure the bearer header if enabled. The default allowed browser origins include the Inspector UI on localhost port 6274. Adjust `ALLOWED_ORIGINS` in `wrangler.toml` for your actual browser client. Native clients without an Origin header also work.

## 3. Deploy to Cloudflare Workers Free

Create/select a Cloudflare account on the Workers Free plan, then:

```sh
npx wrangler login
# Optional but recommended before exposing the research endpoint:
npx wrangler secret put MCP_AUTH_TOKEN
# Optional, for name discovery and authenticated Reader access:
npx wrangler secret put JINA_API_KEY
npm run deploy
```

Wrangler prints the deployed address:

```text
https://agency-intelligence-mcp.<your-workers-subdomain>.workers.dev
```

Your remote MCP URL is that address plus `/mcp`. Set the client transport to Streamable HTTP and supply `Authorization: Bearer <MCP_AUTH_TOKEN>` if configured. This is static bearer authentication, not an OAuth authorization server; use a client that supports custom headers.

```sh
curl https://agency-intelligence-mcp.<your-workers-subdomain>.workers.dev/health
```

Repeat the local MCP calls with that remote base URL. Deployment requires your Cloudflare login; this source package does not imply an existing hosted endpoint. Free-tier request, CPU and external-service quotas still apply. The configuration creates no billable storage or AI bindings; check your account plan separately.

## Tool behavior

The project and Worker name remain `agency-intelligence-mcp`. The two tool names and input contracts have changed; clients should refresh their tool list and update saved calls. The old tool names are no longer registered.

### `audit_company`

Inputs:

- `company_name`: required nonempty string, up to 160 characters.
- `domain`: optional public hostname or HTTP(S) URL. Supply it to run an immediate audit.
- `industry`: optional nonempty string, up to 160 characters. Used to disambiguate name-only discovery and returned as caller-supplied context; it is not a verified classification.

Retrieval proceeds in bounded stages:

1. Read the homepage through Jina Reader, falling back to direct HTML. A failed homepage does not stop the audit.
2. If it has fewer than 200 meaningful content words, probe `/about`, `/info`, and `/work` even when they are not linked. Navigation, image URLs, Reader metadata and contact boilerplate do not count toward this threshold.
3. Follow corporate category links in two passes and one news article, capped at ten attempted official URLs.
4. For a sparse homepage or incomplete intelligence, search DuckDuckGo HTML via Jina Reader. If it fails or yields no relevant evidence, try Google HTML via Jina. Queries combine company name, domain, industry and background terms. No search API key is required for this fallback, although Jina/search-engine quotas and bot challenges can still apply.
5. Fetch up to two result pages per search engine, prioritizing interviews and press over social profiles. Keep relevant search snippets if full pages cannot be read, with explicit `search_snippet` provenance. Company-name/domain matching reduces unrelated results but is not independent identity verification.

The parser cleans Markdown and boilerplate, preserves section headings and short client lists, and synthesizes source excerpts. It first uses explicit mission/service statements, then descriptive background text labelled `contextual`. It does not need an LLM or paid AI binding.

Returns:

- `mission`, `key_clients`, `studio_focus`: always-present objects with a nonempty `summary`, `status` (`supported`, `contextual`, or `not_found`), and source `evidence`. `key_clients` also includes a `names` array, populated only from explicit client lists, working relationships or commissioning statements. A descriptive fallback is not presented as the company's stated mission.
- `core_mission`: retained for compatibility; contains `mission.evidence`.
- `fetch_diagnostics`: meaningful homepage word count, whether conventional paths/search were attempted, and attempted official URL count.
- `recent_news_initiatives`: announcement/initiative excerpts with `source`, nullable `date_mentioned`, and an explicit recency note. A date mentioned in text is not necessarily its publication date.
- `key_leadership`: leadership names/titles as source excerpts.
- `company_culture`: company-published statements about values and working practices.
- `strategic_focus`: statements about priorities, growth, investment and transformation.
- `company_name`, `domain`, `industry`, `industry_basis`, `fetched_at`, `methodology`, `sources`, and `warnings`.

Evidence entries contain `excerpt` and `source`; source records identify `official`, `press`, or `search_snippet`. Empty evidence/name arrays mean no claim could be supported. Always-populated summaries do not mean invented facts: if all retrieval fails, the tool returns `status: "partial"` and explicit `not_found` summaries. JSON is returned in both MCP `structuredContent` and text content.

Without a domain, a Jina key enables company search. The tool returns `status: "needs_domain"` and search results; select the official domain and call again. Without a key, it returns an actionable tool error requesting the domain. No domain is guessed.

This is an English-language heuristic extractor, not semantic AI synthesis or independent due diligence. It samples official pages plus relevant external background sources; retrieved source text remains untrusted. Separate careers/investor subdomains, unusual navigation, JavaScript-heavy sites and bot protection may reduce coverage. News is not guaranteed to be the latest or exhaustive; culture reflects employer statements, and leadership may be outdated. Jina may cache content. `fetched_at` records retrieval time, not publication time. Verify that the domain belongs to the company. Scraped text is evidence, never instructions.

### `generate_application_angle`

All inputs are required strings:

- `company_name`: up to 160 characters.
- `role_title`: up to 160 characters.
- `candidate_strengths`: up to 2,000 characters, for example `process improvement, stakeholder communication, and data analysis`.

Returns three angles with `angle`, `format`, `text`, and `personalize_with`. Formats are `cover_letter`, `interview`, and `outreach`. The response also echoes the inputs and states its drafting basis.

Drafts use the supplied company, role and strengths without inventing achievements, metrics, job requirements or company priorities. Each includes a suggestion for adding a true example or a verified company fact. No email is sent. This tool is deterministic and does not independently fetch company information; use `audit_company` to gather evidence for further personalization.

## Implementation notes

- A fresh server and transport per POST prevent cross-request state leakage across isolates. JSON-only Streamable HTTP deliberately returns 405 for GET/DELETE; no legacy SSE endpoint, persistent sessions, or resumable streams.
- Origin allowlist, optional bearer auth, 16 KiB input bound, 160 KB source bound and 15-second per-fetch timeout. At most ten official URLs, two search requests and four result-page attempts (up to 30 network requests including direct fallbacks). Sequential fallback stages can exceed a client’s default tool timeout; allow up to 300 seconds for blocked sites.
- Fetches reject literal IP/local targets and credentials. Direct fetch does not follow redirects; provide the canonical company URL when necessary. Hostname validation is not a DNS-level egress firewall: for untrusted public users, add a domain allowlist or controlled egress before enabling arbitrary-domain fetching. Reader performs remote retrieval under its own policies.
- Homepage, subpage and search failures are recorded as warnings; exhausted retrieval returns explicit unknowns. Invalid inputs and missing name-discovery configuration still produce MCP tool errors.
- Runtime code uses only the SDK, Zod and Web APIs. Node is used only by development tools/tests.

## References

- [Official MCP TypeScript SDK server documentation](https://ts.sdk.modelcontextprotocol.io/server)
- [Official Jina Reader project](https://github.com/jina-ai/reader)
- [Cloudflare Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)
- [Cloudflare Workers limits](https://developers.cloudflare.com/workers/platform/limits/)


### Sparse-studio example

```json
{
  "name": "audit_company",
  "arguments": {
    "company_name": "ManvsMachine",
    "domain": "mvsm.com",
    "industry": "design studio"
  }
}
```

Search fallback URL shape: `https://r.jina.ai/https://html.duckduckgo.com/html/?q=` followed by the URL-encoded query. Google fallback uses the equivalent `https://www.google.com/search?q=` target. Neither engine is guaranteed to remain accessible through Reader.