harness-mcp-v2 3.2.24 → 3.2.26
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 +128 -27
- package/build/client/harness-client.d.ts +4 -0
- package/build/client/harness-client.d.ts.map +1 -1
- package/build/client/harness-client.js +69 -17
- package/build/client/harness-client.js.map +1 -1
- package/build/client/sse.d.ts +9 -0
- package/build/client/sse.d.ts.map +1 -0
- package/build/client/sse.js +103 -0
- package/build/client/sse.js.map +1 -0
- package/build/client/types.d.ts +10 -2
- package/build/client/types.d.ts.map +1 -1
- package/build/config.d.ts +24 -5
- package/build/config.d.ts.map +1 -1
- package/build/config.js +61 -9
- package/build/config.js.map +1 -1
- package/build/data/schemas/v0/pipeline.d.ts.map +1 -1
- package/build/data/schemas/v0/pipeline.js +118 -78
- package/build/data/schemas/v0/pipeline.js.map +1 -1
- package/build/data/schemas/v0/template.d.ts.map +1 -1
- package/build/data/schemas/v0/template.js +118 -78
- package/build/data/schemas/v0/template.js.map +1 -1
- package/build/data/schemas/v1/template.d.ts.map +1 -1
- package/build/data/schemas/v1/template.js +7 -5
- package/build/data/schemas/v1/template.js.map +1 -1
- package/build/index.js +50 -7
- package/build/index.js.map +1 -1
- package/build/registry/extractors.d.ts +37 -0
- package/build/registry/extractors.d.ts.map +1 -1
- package/build/registry/extractors.js +189 -0
- package/build/registry/extractors.js.map +1 -1
- package/build/registry/index.d.ts.map +1 -1
- package/build/registry/index.js +5 -0
- package/build/registry/index.js.map +1 -1
- package/build/registry/scope-utils.d.ts +5 -4
- package/build/registry/scope-utils.d.ts.map +1 -1
- package/build/registry/scope-utils.js +8 -4
- package/build/registry/scope-utils.js.map +1 -1
- package/build/registry/toolsets/access-control.d.ts.map +1 -1
- package/build/registry/toolsets/access-control.js +1 -0
- package/build/registry/toolsets/access-control.js.map +1 -1
- package/build/registry/toolsets/agents.d.ts.map +1 -1
- package/build/registry/toolsets/agents.js +20 -2
- package/build/registry/toolsets/agents.js.map +1 -1
- package/build/registry/toolsets/autonomous_work.d.ts.map +1 -1
- package/build/registry/toolsets/autonomous_work.js +126 -26
- package/build/registry/toolsets/autonomous_work.js.map +1 -1
- package/build/registry/toolsets/feature-flags.d.ts.map +1 -1
- package/build/registry/toolsets/feature-flags.js +220 -24
- package/build/registry/toolsets/feature-flags.js.map +1 -1
- package/build/registry/toolsets/file-store.d.ts.map +1 -1
- package/build/registry/toolsets/file-store.js +6 -49
- package/build/registry/toolsets/file-store.js.map +1 -1
- package/build/registry/toolsets/registries-v3.d.ts +3 -0
- package/build/registry/toolsets/registries-v3.d.ts.map +1 -0
- package/build/registry/toolsets/registries-v3.js +587 -0
- package/build/registry/toolsets/registries-v3.js.map +1 -0
- package/build/registry/toolsets/repositories.d.ts.map +1 -1
- package/build/registry/toolsets/repositories.js +31 -5
- package/build/registry/toolsets/repositories.js.map +1 -1
- package/build/registry/toolsets/services.d.ts.map +1 -1
- package/build/registry/toolsets/services.js +27 -5
- package/build/registry/toolsets/services.js.map +1 -1
- package/build/registry/toolsets/vibe.d.ts +3 -0
- package/build/registry/toolsets/vibe.d.ts.map +1 -0
- package/build/registry/toolsets/vibe.js +166 -0
- package/build/registry/toolsets/vibe.js.map +1 -0
- package/build/registry/types.d.ts +5 -3
- package/build/registry/types.d.ts.map +1 -1
- package/build/utils/base64.d.ts +21 -0
- package/build/utils/base64.d.ts.map +1 -0
- package/build/utils/base64.js +77 -0
- package/build/utils/base64.js.map +1 -0
- package/build/utils/body-normalizer.d.ts +10 -12
- package/build/utils/body-normalizer.d.ts.map +1 -1
- package/build/utils/body-normalizer.js +9 -9
- package/build/utils/body-normalizer.js.map +1 -1
- package/build/utils/deep-links.d.ts.map +1 -1
- package/build/utils/deep-links.js +5 -6
- package/build/utils/deep-links.js.map +1 -1
- package/build/utils/errors.d.ts +2 -2
- package/build/utils/errors.d.ts.map +1 -1
- package/build/utils/errors.js.map +1 -1
- package/build/utils/http-auth.d.ts +2 -1
- package/build/utils/http-auth.d.ts.map +1 -1
- package/build/utils/http-auth.js +13 -2
- package/build/utils/http-auth.js.map +1 -1
- package/build/utils/oauth-auth.d.ts +15 -0
- package/build/utils/oauth-auth.d.ts.map +1 -0
- package/build/utils/oauth-auth.js +204 -0
- package/build/utils/oauth-auth.js.map +1 -0
- package/build/utils/url-parser.d.ts.map +1 -1
- package/build/utils/url-parser.js +51 -1
- package/build/utils/url-parser.js.map +1 -1
- package/npm-shrinkwrap.json +6 -6
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://mcptoplist.com/server/glama%2Fharness%2Fmcp-server)
|
|
4
4
|
|
|
5
|
-
An MCP (Model Context Protocol) server that gives AI agents full access to the Harness.io platform through 11 consolidated tools and
|
|
5
|
+
An MCP (Model Context Protocol) server that gives AI agents full access to the Harness.io platform through 11 consolidated tools and 245 resource types.
|
|
6
6
|
|
|
7
7
|
## Why Use This MCP Server
|
|
8
8
|
|
|
@@ -10,8 +10,8 @@ Most MCP servers map one tool per API endpoint. For a platform as broad as Harne
|
|
|
10
10
|
|
|
11
11
|
This server is built differently:
|
|
12
12
|
|
|
13
|
-
- **11 tools,
|
|
14
|
-
- **Full platform coverage.**
|
|
13
|
+
- **11 tools, 245 resource types.** A registry-based dispatch system routes `harness_list`, `harness_get`, `harness_create`, etc. to any Harness resource — pipelines, services, environments, orgs, projects, feature flags, cost data, and more. The LLM picks from 11 tools instead of hundreds.
|
|
14
|
+
- **Full platform coverage.** 41 default toolsets spanning CI/CD, GitOps, Feature Flags, Cloud Cost Management, Security Testing, Chaos Engineering, Database DevOps, Internal Developer Portal, Software Supply Chain, Infrastructure as Code Management, Release Management, Governance, Service Overrides, Knowledge Graph, and more. Opt-in Ansible coverage is available when you need inventory and playbook data.
|
|
15
15
|
- **Multi-project workflows out of the box.** Agents discover organizations and projects dynamically — no hardcoded env vars needed. Ask "show failed executions across all projects" and the agent can navigate the full account hierarchy.
|
|
16
16
|
- **35 prompt templates.** Pre-built prompts for common workflows: build & deploy apps end-to-end, debug failed pipelines, review DORA metrics, triage vulnerabilities, optimize cloud costs, audit access control, plan feature flag rollouts, review pull requests, approve pending pipelines, and more.
|
|
17
17
|
- **Works everywhere.** Stdio transport for local clients (Claude Desktop, Cursor, Devin Desktop), HTTP transport for remote/shared deployments, Docker and Kubernetes ready.
|
|
@@ -123,14 +123,17 @@ When running in HTTP mode, the server exposes:
|
|
|
123
123
|
| `/mcp` | `DELETE` | Terminate an active MCP session |
|
|
124
124
|
| `/mcp` | `OPTIONS` | CORS preflight |
|
|
125
125
|
| `/health` | `GET` | Health check — returns `{ "status": "ok", "sessions": <count> }` |
|
|
126
|
+
| `/.well-known/oauth-protected-resource` | `GET` | RFC 9728 metadata when `HARNESS_MCP_MODE=oauth` |
|
|
127
|
+
| `/.well-known/oauth-protected-resource/mcp` | `GET` | Path-aware RFC 9728 metadata for the default `/mcp` resource |
|
|
126
128
|
|
|
127
129
|
|
|
128
130
|
The HTTP transport runs in **session-based mode**. A new MCP session is created on `initialize`, the server returns an `mcp-session-id` header, and subsequent requests for that session must include the same header.
|
|
129
131
|
|
|
130
132
|
Operational constraints in HTTP mode:
|
|
131
133
|
|
|
132
|
-
- Set `HARNESS_MCP_AUTH_TOKEN` for
|
|
133
|
-
-
|
|
134
|
+
- Set `HARNESS_MCP_AUTH_TOKEN` for shared or remotely reachable single-user and multi-user deployments. When set, every `POST`, `GET`, and `DELETE` request to `/mcp` must include `Authorization: Bearer <token>`.
|
|
135
|
+
- OAuth mode accepts HarnessID access tokens instead of `HARNESS_MCP_AUTH_TOKEN` and can bind to a non-loopback address without the unauthenticated opt-out.
|
|
136
|
+
- Non-loopback single-user and multi-user binds require `HARNESS_MCP_AUTH_TOKEN` by default. To run unauthenticated on a non-loopback interface anyway, set `HARNESS_MCP_ALLOW_UNAUTHENTICATED_HTTP=true` explicitly.
|
|
134
137
|
- `POST /mcp` without `mcp-session-id` must be an `initialize` request.
|
|
135
138
|
- `POST /mcp`, `GET /mcp`, and `DELETE /mcp` for existing sessions require the `mcp-session-id` header.
|
|
136
139
|
- `GET /mcp` is used for SSE notifications (progress updates and elicitation prompts).
|
|
@@ -140,6 +143,45 @@ Operational constraints in HTTP mode:
|
|
|
140
143
|
- Set `x-harness-pipeline-version: 0` or `1` on the `initialize` request to select V0 or V1 pipeline resources for that HTTP session.
|
|
141
144
|
- Set `x-harness-auto-approve-risk: none|low_write|medium_write|high_write|all` on the `initialize` request to choose a stricter per-session auto-approval threshold. The server caps this value at the deployment-level `HARNESS_AUTO_APPROVE_RISK`, so a session can reduce but not expand the configured approval ceiling.
|
|
142
145
|
|
|
146
|
+
#### HarnessID OAuth Mode
|
|
147
|
+
|
|
148
|
+
Set `HARNESS_MCP_MODE=oauth` to let remote MCP clients discover HarnessID and complete OAuth 2.1 Authorization Code with PKCE. OAuth mode is available only with HTTP transport. Production HarnessID, MCP resource, and API routing defaults are built in:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
HARNESS_MCP_MODE=oauth
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This defaults to the issuer `https://id.harness.io/idp/realms/HarnessIDP`, resource `https://mcp.harness.io/mcp`, OAuth client `mcp-client`, and Harness API base `https://mcp.harness.io/cli`. Override them only for QA, local development, or another Harness environment.
|
|
155
|
+
|
|
156
|
+
`HARNESS_API_KEY` must not be set in this mode. `HARNESS_MCP_OAUTH_JWKS_URI` defaults to `<issuer>/protocol/openid-connect/certs`, and `HARNESS_ACCOUNT_ID` is unnecessary because the account comes from the token.
|
|
157
|
+
|
|
158
|
+
The server publishes RFC 9728 protected-resource metadata and returns this challenge when a client has not authenticated:
|
|
159
|
+
|
|
160
|
+
```http
|
|
161
|
+
HTTP/1.1 401 Unauthorized
|
|
162
|
+
WWW-Authenticate: Bearer resource_metadata="https://mcp.harness.io/.well-known/oauth-protected-resource/mcp"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
It validates the HarnessID access token's RS256 signature, `iss`, expiry, and `sub` using the configured JWKS endpoint, and checks that the token was issued to `HARNESS_MCP_OAUTH_CLIENT_ID` through the `azp` claim. `HARNESS_MCP_OAUTH_RESOURCE` is the RFC 9728 protected-resource identifier used for discovery and challenges. Current HarnessID access tokens use `aud: account` rather than the MCP URL, so the resource is not compared with `aud`.
|
|
166
|
+
|
|
167
|
+
The account ID comes from the token's `HARNESS_MCP_OAUTH_ACCOUNT_CLAIM` claim (`account_id` by default), which the HarnessID `organization` scope populates. Each session stores the caller's access token and forwards it to the Harness API as `Authorization: Bearer`, so Harness RBAC and audit records reflect the logged-in user rather than a shared PAT. The session is bound to the `sub` and account it was created with: a later request may carry a refreshed token, but one for a different user or account is rejected.
|
|
168
|
+
|
|
169
|
+
Clients normally need only the MCP resource URL:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"mcpServers": {
|
|
174
|
+
"harness": {
|
|
175
|
+
"url": "https://mcp.harness.io/mcp"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The client reads the protected-resource metadata, discovers `HARNESS_MCP_OAUTH_ISSUER`, and then uses that authorization server's RFC 8414 metadata. If the client does not support dynamic client registration, use the pre-registered `mcp-client` client ID.
|
|
182
|
+
|
|
183
|
+
See [HarnessID OAuth for a self-hosted MCP server](docs/harnessid-oauth.md) for the QA Keycloak checklist and validation commands.
|
|
184
|
+
|
|
143
185
|
#### Multi-User Mode
|
|
144
186
|
|
|
145
187
|
Set `HARNESS_MCP_MODE=multi-user` for shared HTTP deployments where each client authenticates as a different Harness user. In this mode:
|
|
@@ -502,13 +544,10 @@ pnpm docker:run
|
|
|
502
544
|
docker run --rm -p 3000:3000 \
|
|
503
545
|
-e HARNESS_API_KEY=pat.xxx.xxx.xxx \
|
|
504
546
|
-e HARNESS_ACCOUNT_ID=your-account-id \
|
|
505
|
-
-e HARNESS_MCP_AUTH_TOKEN=replace-with-a-long-random-token \
|
|
506
547
|
harness-mcp-server
|
|
507
548
|
```
|
|
508
549
|
|
|
509
550
|
The container runs in HTTP mode on port 3000 by default with a built-in health check.
|
|
510
|
-
It binds to the container network, so set `HARNESS_MCP_AUTH_TOKEN` in `.env` or the
|
|
511
|
-
container environment and send that value as `Authorization: Bearer <token>` on MCP requests.
|
|
512
551
|
|
|
513
552
|
### Kubernetes
|
|
514
553
|
|
|
@@ -538,11 +577,17 @@ The server automatically loads environment variables from a `.env` file in the p
|
|
|
538
577
|
|
|
539
578
|
| Variable | Required | Default | Description |
|
|
540
579
|
| --------------------------- | -------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
541
|
-
| `HARNESS_MCP_MODE` | No | `single-user` | Deployment mode: `single-user` (API key
|
|
542
|
-
| `HARNESS_API_KEY` | Yes* | -- | Harness personal access token or service account token. Required in `single-user` mode. Must NOT be set in `multi-user` mode
|
|
580
|
+
| `HARNESS_MCP_MODE` | No | `single-user` | Deployment mode: `single-user` (shared API key), `multi-user` (HTTP with per-session API keys), or `oauth` (HTTP with HarnessID access-token validation) |
|
|
581
|
+
| `HARNESS_API_KEY` | Yes* | -- | Harness personal access token or service account token. Required in `single-user` mode. Must NOT be set in `multi-user` or `oauth` mode, where each session brings its own credential |
|
|
543
582
|
| `HARNESS_ACCOUNT_ID` | No | *(from PAT/SAT)* | Harness account identifier. Auto-extracted from PAT/SAT tokens in single-user mode; multi-user sessions can provide their own via `x-harness-account-id` when the API key does not embed one |
|
|
544
|
-
| `HARNESS_BASE_URL` | No | `https://app.harness.io`
|
|
545
|
-
| `
|
|
583
|
+
| `HARNESS_BASE_URL` | No | `https://app.harness.io` (`https://mcp.harness.io/cli` in OAuth mode) | Harness API/UI base URL. OAuth mode routes through the hosted MCP `/cli` proxy by default; other modes use the Harness SaaS API directly |
|
|
584
|
+
| `HARNESS_MCP_OAUTH_ISSUER` | No | `https://id.harness.io/idp/realms/HarnessIDP` | HarnessID issuer matched exactly against the access token `iss` claim |
|
|
585
|
+
| `HARNESS_MCP_OAUTH_RESOURCE` | No | `https://mcp.harness.io/mcp` | Public canonical MCP URL published as the RFC 9728 resource identifier |
|
|
586
|
+
| `HARNESS_MCP_OAUTH_JWKS_URI` | No | `<issuer>/protocol/openid-connect/certs` | HarnessID JWKS endpoint used to validate RS256 access-token signatures |
|
|
587
|
+
| `HARNESS_MCP_OAUTH_CLIENT_ID` | No | `mcp-client` | HarnessID client the access token must be issued to, checked against the token's `azp` claim |
|
|
588
|
+
| `HARNESS_MCP_OAUTH_ACCOUNT_CLAIM` | No | `account_id` | Access-token claim carrying the Harness account ID, populated by the HarnessID `organization` scope |
|
|
589
|
+
| `HARNESS_MCP_OAUTH_SCOPES` | No | `openid profile email organization` | Space-separated scopes advertised in RFC 9728 protected-resource metadata |
|
|
590
|
+
| `HARNESS_FME_API_KEY` | No | -- | Optional single-user/self-hosted FME/Split Admin credential used for `fme_` resources in **legacy (`workspace_id`) mode only**. Legacy FME is unavailable in OAuth mode so HarnessID tokens are never sent to `api.split.io`; use Harness-native `org_id`+`project_id` scope instead. Must not be set in `multi-user` or `oauth` mode |
|
|
546
591
|
| `HARNESS_FME_BASE_URL` | No | `https://api.split.io` | Split/FME Admin API base URL used by `fme_` resources in **legacy (`workspace_id`) mode only**. HTTP URLs require `HARNESS_ALLOW_HTTP=true` for local development. Harness-native (`org_id`+`project_id`) mode ignores this and uses the standard `HARNESS_API_KEY`/`HARNESS_BASE_URL` instead |
|
|
547
592
|
| `HARNESS_ORG` | No | -- | Organization ID. Used when `org_id` is not specified per tool call. If omitted, `org_id` must be provided explicitly. Agents can also discover orgs dynamically via `harness_list(resource_type="organization")` |
|
|
548
593
|
| `HARNESS_PROJECT` | No | -- | Project ID. Used when `project_id` is not specified per tool call. Agents can also discover projects dynamically via `harness_list(resource_type="project")` |
|
|
@@ -558,7 +603,7 @@ The server automatically loads environment variables from a `.env` file in the p
|
|
|
558
603
|
| `HARNESS_ALLOW_HTTP` | No | `false` | Allow non-HTTPS `HARNESS_BASE_URL`. By default, the server enforces HTTPS for security. Set to `true` only for local development against a non-TLS Harness instance |
|
|
559
604
|
| `HARNESS_PIPELINE_VERSION` | No | `0` | **(Alpha)** Pipeline YAML version. `0` loads the `pipeline` resource type and excludes `pipeline_v1`; `1` loads `pipeline_v1` and excludes `pipeline`. HTTP sessions can override this at initialize time with `x-harness-pipeline-version: 0` or `1` |
|
|
560
605
|
| `HARNESS_MCP_ALLOWED_HOSTS` | No | -- | Comma-separated hostnames allowed by HTTP transport Host-header validation. `mcp.harness.io` is allowed by default for localhost binds; add proxy/custom domains here |
|
|
561
|
-
| `HARNESS_MCP_AUTH_TOKEN` | No | -- | Bearer token required on `/mcp` HTTP routes when set. Required by default
|
|
606
|
+
| `HARNESS_MCP_AUTH_TOKEN` | No | -- | Static Bearer token required on `/mcp` HTTP routes when set. Required by default for non-loopback single-user and multi-user binds. Must be unset in `oauth` mode |
|
|
562
607
|
| `HARNESS_MCP_ALLOW_UNAUTHENTICATED_HTTP` | No | `false` | Explicitly allow unauthenticated HTTP transport on non-loopback binds. Use only behind another authenticated control |
|
|
563
608
|
| `HARNESS_MCP_TRUST_PROXY` | No | `0` | Number of reverse proxy / load balancer hops to trust for client IP resolution (Express `trust proxy`). Set to the proxy count in front of the server so per-IP rate limiting keys on the real client rather than the proxy socket peer |
|
|
564
609
|
| `HARNESS_MCP_LOG_FILE` | No | `~/.claude/harness-mcp.log` | File used for stdio disconnect/crash diagnostics when stderr may no longer be available |
|
|
@@ -669,7 +714,7 @@ Current multi-scope resources include `connector`, `service`, `environment`, `in
|
|
|
669
714
|
| Tool | Description |
|
|
670
715
|
| --------------------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
671
716
|
| `harness_describe` | Discover available resource types, operations, and fields. No API call — returns local registry metadata. |
|
|
672
|
-
| `harness_schema` | Fetch exact YAML/JSON Schema definitions and examples for creating/updating resources. Pipeline/template schemas are bundled; connector, environment, service, secret, and infrastructure schemas are scope-aware entity schemas fetched from bundled snapshots or NG `/yaml-schema`; `release_process` and `release_activity` schemas are fetched live from `/
|
|
717
|
+
| `harness_schema` | Fetch exact YAML/JSON Schema definitions and examples for creating/updating resources. Pipeline/template schemas are bundled; connector, environment, service, secret, and infrastructure schemas are scope-aware entity schemas fetched from bundled snapshots or NG `/yaml-schema`; `release_process` and `release_activity` schemas are fetched live from RMG `/api/yamlSchema`. Supports deep drilling via `path`. |
|
|
673
718
|
| `harness_list` | List resources of a given type with filtering, search, and pagination. |
|
|
674
719
|
| `harness_get` | Get a single resource by its identifier. |
|
|
675
720
|
| `harness_create` | Create a new resource. Supports inline and remote (Git-backed) pipelines. Prompts for user confirmation via [elicitation](#elicitation). |
|
|
@@ -687,7 +732,7 @@ Use `harness_schema` before creating or updating YAML-backed resources so agents
|
|
|
687
732
|
|
|
688
733
|
- Bundled schemas include `pipeline`, `template`, `trigger`, `pipeline_v1`, `template_v1`, `inputSet_v1`, `overlayInputSet_v1`, and `agent-pipeline`.
|
|
689
734
|
- Entity schemas include `connector`, `environment`, `service`, `secret`, and `infrastructure`. They are scope-aware (`account`, `org`, or `project`) and require `org_id`/`project_id` when the selected scope requires them.
|
|
690
|
-
- Release Management definitions (`release_process`, `release_activity`) fetch live JSON Schema from `/
|
|
735
|
+
- Release Management definitions (`release_process`, `release_activity`) fetch live JSON Schema from RMG `/api/yamlSchema` (not bundled). Pass `scope`, `org_id`, and `project_id` when scoping to org or project.
|
|
691
736
|
- Vendored entity snapshots are used first when they match the runtime account; otherwise the tool falls back to the Harness NG `/yaml-schema` API and caches the result.
|
|
692
737
|
- Omit `path` for a field/section summary, then pass a dot-separated `path` to inspect a nested definition.
|
|
693
738
|
|
|
@@ -912,7 +957,7 @@ For v1 pipelines:
|
|
|
912
957
|
|
|
913
958
|
1. Fetch `harness_get(resource_type="runtime_input_template_v1", resource_id="<pipeline_id>")`.
|
|
914
959
|
For Git-backed pipelines, pass `branch_name`, `connector_ref`, and `repo_name` through `params`.
|
|
915
|
-
2.
|
|
960
|
+
2. Use each returned `inputs[].details.name` as a top-level key in `harness_execute.inputs`.
|
|
916
961
|
3. Run `harness_execute(resource_type="pipeline_v1", action="run", resource_id="<pipeline_id>", inputs={...})`.
|
|
917
962
|
The server wraps these values under an `inputs:` YAML root and sends the API's `inputs_yaml` body.
|
|
918
963
|
|
|
@@ -1216,7 +1261,7 @@ Harness pipelines can be stored in three ways:
|
|
|
1216
1261
|
|
|
1217
1262
|
## Resource Types
|
|
1218
1263
|
|
|
1219
|
-
|
|
1264
|
+
245 resource types organized across 41 toolsets. Each resource type supports a subset of CRUD operations and optional execute actions.
|
|
1220
1265
|
|
|
1221
1266
|
### Platform
|
|
1222
1267
|
|
|
@@ -1505,7 +1550,7 @@ Use `harness_execute(resource_type="pull_request", action="close", ...)` for an
|
|
|
1505
1550
|
|
|
1506
1551
|
Release Management (RMG) resources are default-enabled. Definition resources (`release_process`, `release_activity`) support list/get/create/update/delete with `body.yaml`; call `harness_schema(resource_type="release_process"|"release_activity")` before create/update. Execution resources monitor running releases — most list operations require `release_id` (UUID from `harness_list resource_type=release`, or the UI URL slug such as `identifier-1.0.0-abc`). Paste an RMG release URL into `harness_list` to auto-fill `release_id`.
|
|
1507
1552
|
|
|
1508
|
-
RMG
|
|
1553
|
+
RMG calls use `${HARNESS_BASE_URL}/gateway/rmg` with account scoping via the `Harness-Account` header. Org/project scope uses header-based scoping when `org_id`/`project_id` are provided. `release_execution_phase` is list-only — use each phase item's `identifier` field as `params.phase_identifier` when calling `harness_get` on phase input/output resources (do not call `harness_get` on `release_execution_phase` itself). Release list `status` filtering is applied client-side on the current page only; keep paging with the same filters when results may span pages.
|
|
1509
1554
|
|
|
1510
1555
|
| Resource Type | List | Get | Create | Update | Delete | Execute Actions |
|
|
1511
1556
|
| ------------------------------------- | ---- | --- | ------ | ------ | ------ | --------------- |
|
|
@@ -1531,6 +1576,58 @@ Typical workflow:
|
|
|
1531
1576
|
6. `harness_get` on `release_input`, `release_execution_phase_input`, `release_execution_phase_output`, `release_execution_activity_output`, or `release_execution_activity_input` using `release_id` plus `params.phase_identifier` / `params.activity_identifier` / `activity_execution_id` as documented on each resource.
|
|
1532
1577
|
|
|
1533
1578
|
|
|
1579
|
+
### Vibe
|
|
1580
|
+
|
|
1581
|
+
The default-enabled `vibe` toolset covers the [Vibe Orchestrator BFF contract](tests/fixtures/vibe-bff-openapi.yaml) under `${HARNESS_BASE_URL}/vibe/v1`. It uses the existing Harness connection and account header, without adding account/org/project query parameters or scope fields to request bodies. The team validated the Vibe flow using Harness API-key authentication (PAT/SAT), so no opt-in setting is required for default sessions. The curated OpenAPI documents bearer/session authentication; the server's OAuth mode forwards the current session's bearer token. Automated regressions verify both header paths; gateway authentication remains subject to the target environment's configuration.
|
|
1582
|
+
|
|
1583
|
+
| Resource Type | List | Get | Create | Update | Delete | Execute Actions |
|
|
1584
|
+
| ------------- | ---- | --- | ------ | ------ | ------ | --------------- |
|
|
1585
|
+
| `vibe_project` | | | x | | | `prepare`, `deploy` |
|
|
1586
|
+
| `vibe_app_lifecycle` | | x | | | | `events` |
|
|
1587
|
+
|
|
1588
|
+
The API supports two intake paths. Keep these API-native request shapes:
|
|
1589
|
+
|
|
1590
|
+
| Source available to the coding agent | API flow |
|
|
1591
|
+
| ----------------------------------- | -------- |
|
|
1592
|
+
| GitHub repository link/connector | `harness_create` with `resource_type="vibe_project"` and `body.mode` plus the mode-specific fields. The contract names `github_link` and `github_connector` but does not define their URL, branch, or connector field shapes; these fields are forwarded to the backend without inventing a mapping. |
|
|
1593
|
+
| ZIP file | Call `prepare` with the app name and file metadata, upload the bytes to the returned signed target, then call `deploy`. |
|
|
1594
|
+
| Local source directory | The coding agent archives the intended workspace source into a ZIP locally, then follows the ZIP flow. A local path or conversational context is not an API-supported source upload. |
|
|
1595
|
+
|
|
1596
|
+
When packaging a directory, include the source, manifests, lockfiles, configuration, and intended uncommitted edits needed to build it. Exclude credentials, `.git`, installed dependencies, and generated artifacts. Packaging and the signed upload happen where the files are accessible; a hosted MCP server cannot read the coding agent's local directory.
|
|
1597
|
+
|
|
1598
|
+
For an existing ZIP, prepare the upload:
|
|
1599
|
+
|
|
1600
|
+
```json
|
|
1601
|
+
{
|
|
1602
|
+
"resource_type": "vibe_project",
|
|
1603
|
+
"action": "prepare",
|
|
1604
|
+
"body": {
|
|
1605
|
+
"name": "demo-app",
|
|
1606
|
+
"file": {
|
|
1607
|
+
"path": "app.zip",
|
|
1608
|
+
"size_bytes": 12345,
|
|
1609
|
+
"content_type": "application/zip"
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
Pass this to `harness_execute`. The size must describe the actual ZIP; `size_bytes`, `content_type`, and `md5` are optional and nullable. Additional prepare fields are preserved for backend validation, as permitted by the OpenAPI. Preparation returns `projectId`, `sourceId`, and `upload`, including each file's `uploadUrl`, `method`, `headers`, and `expiresAt`. Upload the file bytes directly using that signed URL, method, and headers; preserve the URL exactly and do not add Harness credentials to the storage request. The prepare action does not read or upload local files.
|
|
1616
|
+
|
|
1617
|
+
After a successful upload, explicitly deploy:
|
|
1618
|
+
|
|
1619
|
+
```json
|
|
1620
|
+
{
|
|
1621
|
+
"resource_type": "vibe_project",
|
|
1622
|
+
"action": "deploy",
|
|
1623
|
+
"resource_id": "<projectId returned by prepare>"
|
|
1624
|
+
}
|
|
1625
|
+
```
|
|
1626
|
+
|
|
1627
|
+
For JSON imports, use the returned `id` instead. Deployment also accepts `body: {"project_id": "<Vibe app id>"}` or `params.app_id`; the API wire field is snake_case `project_id` even though prepare returns camelCase `projectId`. The generic tool's top-level `project_id` is a Harness scope identifier and is never used as the Vibe app id. Import and prepare create the app/source; neither starts deployment. Writes are not automatically retried, and deployment uses the existing high-risk confirmation policy.
|
|
1628
|
+
|
|
1629
|
+
Read progress with `harness_get(resource_type="vibe_app_lifecycle", resource_id="<Vibe app id>")`. It retains app URLs, execution stages, substeps, failures, log lines, and build-analyzer details. The `events` execute action accepts `resource_id` or `params.app_id` and consumes the SSE endpoint as a finite batch: up to 20 JSON events or five seconds after connection, with a 1 MiB response limit. These limits belong to the Vibe endpoint. The connection's `HARNESS_API_TIMEOUT_MS` also bounds connection and stream consumption together; expiration returns a timeout error. A completed batch returns `events` and `stop_reason` (`end`, `event_limit`, or `duration_limit`) and closes the stream. Neither initial connection failures nor broken streams are retried. Events are transient diffs with no documented replay cursor; use lifecycle get for an authoritative snapshot. Both lifecycle reads are available in read-only mode.
|
|
1630
|
+
|
|
1534
1631
|
### Feature Flags
|
|
1535
1632
|
|
|
1536
1633
|
|
|
@@ -1547,8 +1644,8 @@ Typical workflow:
|
|
|
1547
1644
|
| `fme_identity` | | | x | x | | |
|
|
1548
1645
|
| `fme_standard_segment` | x | x | | | | |
|
|
1549
1646
|
| `fme_segment_keys` | x | | | x | | |
|
|
1550
|
-
| `fme_segment` | x | x | x |
|
|
1551
|
-
| `fme_segment_definition` | x | x | x | x | x |
|
|
1647
|
+
| `fme_segment` | x | x | x | x | x | |
|
|
1648
|
+
| `fme_segment_definition` | x | x | x | x | x | `list_keys`, `add_keys`, `remove_keys` |
|
|
1552
1649
|
|
|
1553
1650
|
|
|
1554
1651
|
**FME (Split.io) resources** — `fme_`* resources support **dual-mode scoping**: legacy calls pass `workspace_id` and hit the Split.io API (`api.split.io`); newer calls pass `org_id`+`project_id` together and hit Harness-native endpoints (standard `HARNESS_API_KEY`/`HARNESS_BASE_URL`, same auth as every other `harness_*` resource) instead. Passing both `workspace_id` and `org_id`/`project_id` on the same call, or mixing `org_id` with `project_id` alone, is an error — pick one mode per call. Every operation below is available in legacy mode, unchanged. Harness-native mode coverage is currently narrower:
|
|
@@ -1563,10 +1660,10 @@ Typical workflow:
|
|
|
1563
1660
|
- **`fme_rule_based_segment_definition`** — (Deprecated — see `fme_segment_definition`.) Harness-native mode is rejected on every operation/action (`list`/`update`/`enable`/`disable`/`change_request`) — use `fme_segment_definition` instead (no `enable`/`disable`/`change_request` equivalent there); this resource supports only the legacy `workspace_id`/`environment_id` contract.
|
|
1564
1661
|
- **`fme_traffic_type`** — dual-mode `list`. Pass `org_id`+`project_id` (preferred) or the deprecated `workspace_id`. Native pagination uses `offset`/`limit` (max 100; `harness_list` `size` maps to `limit`); results are promoted to `items`/`total`. Each item has `id` and `name` (no `displayAttributeId`).
|
|
1565
1662
|
- **`fme_identity`** — `create`/`update` are not yet implemented if `org_id`+`project_id` are passed together; otherwise proceeds as a normal legacy call.
|
|
1566
|
-
- **`fme_standard_segment`** —
|
|
1567
|
-
- **`fme_segment_keys`** — `list`/`update`
|
|
1568
|
-
- **`fme_segment`** — `list`/`get`/`
|
|
1569
|
-
- **`fme_segment_definition`** —
|
|
1663
|
+
- **`fme_standard_segment`** — deprecated. Legacy `workspace_id` still hits Split v2. Harness-native is rejected — use `fme_segment`.
|
|
1664
|
+
- **`fme_segment_keys`** — `list`/`update` remain legacy (`workspace_id` / `environment_id`+`segment_name`). Harness-native (`org_id`+`project_id`) is rejected — use `fme_segment_definition` execute `list_keys`/`add_keys`/`remove_keys`.
|
|
1665
|
+
- **`fme_segment`** — Native only (`org_id`+`project_id`). CRUD. `list`/`get`/`update`/`delete` require `segment_type`: `STANDARD` | `LARGE` | `RULE_BASED`. Create body: `name`, `trafficType`, `segmentType`; optional `description`, `tags`, `owners`.
|
|
1666
|
+
- **`fme_segment_definition`** — Native only. CRUD plus execute `list_keys`/`add_keys`/`remove_keys`. Update is description only. Delete fails with `hasDependents` while keys remain.
|
|
1570
1667
|
|
|
1571
1668
|
In single-user/self-hosted mode, legacy-mode auth uses a Bearer token from `HARNESS_FME_API_KEY`, falling back to a non-placeholder `HARNESS_API_KEY`. `HARNESS_FME_API_KEY` may be a legacy Split admin key or an FME-entitled Harness PAT/SAT, but it is rejected in `multi-user` mode so shared deployments cannot override each session user's credential. Hosted OAuth/service-routing credentials for Harness platform APIs do not authenticate direct Split.io requests. `fme_feature_flag` supports full lifecycle management in legacy mode: create (requires `traffic_type_id`), list, get, update metadata, delete, and kill/restore/reallocate/archive/unarchive execute actions. Use `fme_traffic_type` to discover traffic type IDs, `fme_identity` to create/update identity attributes, and `fme_standard_segment` / `fme_segment_keys` to inspect standard segments and add member keys. `fme_rule_based_segment` provides CRUD for targeting segments, while `fme_rule_based_segment_definition` manages environment-specific segment rules with enable/disable and change request approval flows.
|
|
1572
1669
|
|
|
@@ -1842,9 +1939,11 @@ Security exemption execute workflow:
|
|
|
1842
1939
|
|
|
1843
1940
|
## Toolset Filtering
|
|
1844
1941
|
|
|
1845
|
-
By default,
|
|
1942
|
+
By default, 41 of 44 toolsets are enabled. Three toolsets are opt-in and excluded from the defaults:
|
|
1846
1943
|
|
|
1847
1944
|
- **`ansible`** — Harness Ansible (inventories, playbooks, hosts, activity). Opt-in because it is project-scoped and adds concepts many users do not need.
|
|
1945
|
+
- **`autonomous_work`** — Development Harness (autonomous work). Opt-in; see toolset description for scope.
|
|
1946
|
+
- **`registries-v3`** — Harness Artifact Registry v3 (packages, versions, files, metadata, scans, firewall exceptions). Opt-in until v3 writes land, so agents don't have to disambiguate between v1 registries/artifacts and v3 packages/versions.
|
|
1848
1947
|
|
|
1849
1948
|
### Adding toolsets with `+` prefix
|
|
1850
1949
|
|
|
@@ -1922,7 +2021,9 @@ Available toolset names:
|
|
|
1922
2021
|
| `ai-evals` | eval_dataset, eval_dataset_item, evaluation, eval_run, eval_run_item, eval_run_by_eval, eval_metric, eval_metric_set, eval_metric_set_entry, eval_suite, eval_suite_evaluation, eval_suite_run, eval_target, eval_annotation, eval_analytics, eval_git_settings, eval_registry_item, eval_git_registration, online_eval |
|
|
1923
2022
|
| `iacm` | iacm_workspace, iacm_variable_set, iacm_resource, iacm_module, iacm_provider, iacm_workspace_costs, iacm_activity_resource_change |
|
|
1924
2023
|
| `ansible` *(opt-in)* | ansible_inventory, ansible_playbook, ansible_host, ansible_host_activity, ansible_activity |
|
|
2024
|
+
| `registries-v3` *(opt-in)* | package_v3, version_v3, file_v3, registry_metadata_v3, package_metadata_v3, version_metadata_v3, file_metadata_v3, metadata_key_v3, metadata_value_v3, artifact_scan_v3, bulk_scan_evaluation_v3, firewall_exception_v3, firewall_exception_version_v3 |
|
|
1925
2025
|
| `release-management` | release_process, release_activity, release, release_execution_phase, release_execution_task, release_execution_activity, release_input, release_execution_phase_input, release_execution_phase_output, release_execution_activity_input, release_execution_activity_output |
|
|
2026
|
+
| `vibe` | vibe_project, vibe_app_lifecycle |
|
|
1926
2027
|
|
|
1927
2028
|
|
|
1928
2029
|
## Architecture
|
|
@@ -1940,8 +2041,8 @@ Available toolset names:
|
|
|
1940
2041
|
|
|
|
1941
2042
|
+--------v---------+
|
|
1942
2043
|
| Registry | <-- Declarative resource definitions
|
|
1943
|
-
|
|
|
1944
|
-
|
|
|
2044
|
+
| 41 Toolsets | (data files, not code)
|
|
2045
|
+
| 245 Resource Types|
|
|
1945
2046
|
+--------+---------+
|
|
1946
2047
|
|
|
|
1947
2048
|
+--------v---------+
|
|
@@ -6,6 +6,7 @@ import type { RequestOptions } from "./types.js";
|
|
|
6
6
|
* AsyncLocalStorage) instead of using the static config value.
|
|
7
7
|
*/
|
|
8
8
|
export type AccountIdResolver = () => string | undefined;
|
|
9
|
+
export type BearerTokenResolver = () => string | undefined;
|
|
9
10
|
export declare class HarnessClient {
|
|
10
11
|
private readonly baseUrl;
|
|
11
12
|
private readonly token;
|
|
@@ -17,6 +18,7 @@ export declare class HarnessClient {
|
|
|
17
18
|
private readonly fmeApiKey;
|
|
18
19
|
private readonly mcpMode;
|
|
19
20
|
private accountIdResolver?;
|
|
21
|
+
private bearerTokenResolver?;
|
|
20
22
|
private currentUserId?;
|
|
21
23
|
private currentUserPromise?;
|
|
22
24
|
constructor(config: Config);
|
|
@@ -25,6 +27,8 @@ export declare class HarnessClient {
|
|
|
25
27
|
* calls this first, falling back to the static config value.
|
|
26
28
|
*/
|
|
27
29
|
setAccountIdResolver(resolver: AccountIdResolver): void;
|
|
30
|
+
/** Set the per-session OAuth access token used for downstream Harness calls. */
|
|
31
|
+
setBearerTokenResolver(resolver: BearerTokenResolver): void;
|
|
28
32
|
/** Resolve the account ID: per-request override → static config fallback. */
|
|
29
33
|
private resolveAccountId;
|
|
30
34
|
get account(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness-client.d.ts","sourceRoot":"","sources":["../../src/client/harness-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAA6C,MAAM,cAAc,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"harness-client.d.ts","sourceRoot":"","sources":["../../src/client/harness-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAA6C,MAAM,cAAc,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAyKjD;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,MAAM,GAAG,SAAS,CAAC;AACzD,MAAM,MAAM,mBAAmB,GAAG,MAAM,MAAM,GAAG,SAAS,CAAC;AAE3D,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,iBAAiB,CAAC,CAAoB;IAC9C,OAAO,CAAC,mBAAmB,CAAC,CAAsB;IAClD,OAAO,CAAC,aAAa,CAAC,CAAS;IAC/B,OAAO,CAAC,kBAAkB,CAAC,CAAkB;gBAEjC,MAAM,EAAE,MAAM;IAY1B;;;OAGG;IACH,oBAAoB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAIvD,gFAAgF;IAChF,sBAAsB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAI3D,6EAA6E;IAC7E,OAAO,CAAC,gBAAgB;IAIxB,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,OAAO,CAAC,YAAY;IAoBpB,OAAO,CAAC,gBAAgB;IAoExB;;;;;;;;OAQG;IACG,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;IA2BnC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAmKrD;;;;OAIG;IACG,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;IA0F/D,OAAO,CAAC,QAAQ;CA8DjB"}
|
|
@@ -2,8 +2,9 @@ import { isPlaceholderCredential, resolveFmeApiKey } from "../config.js";
|
|
|
2
2
|
import { HarnessApiError } from "../utils/errors.js";
|
|
3
3
|
import { RateLimiter } from "../utils/rate-limiter.js";
|
|
4
4
|
import { createLogger } from "../utils/logger.js";
|
|
5
|
-
import { redactJsonString } from "../utils/redact.js";
|
|
6
|
-
import { isFormDataBody } from "../utils/type-guards.js";
|
|
5
|
+
import { redactJsonString, redactSensitiveFields } from "../utils/redact.js";
|
|
6
|
+
import { isFormDataBody, isRecord } from "../utils/type-guards.js";
|
|
7
|
+
import { assertJsonEventStreamLimits, readJsonEventStream } from "./sse.js";
|
|
7
8
|
const log = createLogger("harness-client");
|
|
8
9
|
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
|
|
9
10
|
/**
|
|
@@ -100,6 +101,28 @@ const ERROR_FIELD_ENRICHMENTS = [
|
|
|
100
101
|
{ pathPrefix: "/loadTest/", field: "description" },
|
|
101
102
|
];
|
|
102
103
|
const MAX_ERROR_DETAIL_CHARS = 400;
|
|
104
|
+
/** Preserve standard Harness errors; adapt Vibe's nested BFF envelope only on Vibe paths. */
|
|
105
|
+
function parseApiError(body, path) {
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = JSON.parse(body);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
if (!isRecord(raw))
|
|
114
|
+
return {};
|
|
115
|
+
const nested = path.startsWith("/vibe/v1/") && isRecord(raw.error) ? raw.error : {};
|
|
116
|
+
const message = typeof raw.message === "string" ? raw.message : nested.message;
|
|
117
|
+
const code = raw.code ?? nested.code;
|
|
118
|
+
return {
|
|
119
|
+
...raw,
|
|
120
|
+
message: typeof message === "string" ? message : undefined,
|
|
121
|
+
code: typeof code === "string" || typeof code === "number" ? code : undefined,
|
|
122
|
+
details: raw.details ?? (isRecord(nested.details) ? JSON.stringify(redactSensitiveFields(nested.details)) : nested.details),
|
|
123
|
+
correlationId: typeof raw.correlationId === "string" ? raw.correlationId : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
103
126
|
/** Extra NG fields (`details`, `detailedMessage`, `responseMessages`) not already in `message`. */
|
|
104
127
|
function collectErrorDetail(parsed, message) {
|
|
105
128
|
const seen = new Set([message.trim()]);
|
|
@@ -148,6 +171,7 @@ export class HarnessClient {
|
|
|
148
171
|
fmeApiKey;
|
|
149
172
|
mcpMode;
|
|
150
173
|
accountIdResolver;
|
|
174
|
+
bearerTokenResolver;
|
|
151
175
|
currentUserId;
|
|
152
176
|
currentUserPromise;
|
|
153
177
|
constructor(config) {
|
|
@@ -168,6 +192,10 @@ export class HarnessClient {
|
|
|
168
192
|
setAccountIdResolver(resolver) {
|
|
169
193
|
this.accountIdResolver = resolver;
|
|
170
194
|
}
|
|
195
|
+
/** Set the per-session OAuth access token used for downstream Harness calls. */
|
|
196
|
+
setBearerTokenResolver(resolver) {
|
|
197
|
+
this.bearerTokenResolver = resolver;
|
|
198
|
+
}
|
|
171
199
|
/** Resolve the account ID: per-request override → static config fallback. */
|
|
172
200
|
resolveAccountId() {
|
|
173
201
|
return this.accountIdResolver?.() ?? this.accountId;
|
|
@@ -196,6 +224,21 @@ export class HarnessClient {
|
|
|
196
224
|
return headers;
|
|
197
225
|
}
|
|
198
226
|
applyDefaultAuth(headers, isFme) {
|
|
227
|
+
if (this.mcpMode === "oauth") {
|
|
228
|
+
deleteHeaderValues(headers, "x-api-key");
|
|
229
|
+
if (isFme) {
|
|
230
|
+
throw new HarnessApiError("Legacy workspace-based FME calls are unavailable in oauth mode. " +
|
|
231
|
+
"Pass org_id and project_id to use the Harness-native FME API.", 401, "FME_AUTH_MISSING");
|
|
232
|
+
}
|
|
233
|
+
if (getHeaderValue(headers, "authorization"))
|
|
234
|
+
return;
|
|
235
|
+
const token = this.bearerTokenResolver?.();
|
|
236
|
+
if (!token) {
|
|
237
|
+
throw new HarnessApiError("OAuth access token is unavailable for this MCP session.", 401, "OAUTH_TOKEN_MISSING");
|
|
238
|
+
}
|
|
239
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
199
242
|
if (isFme) {
|
|
200
243
|
// FME/Split Admin APIs expect Bearer auth. Drop x-api-key here so
|
|
201
244
|
// placeholder credentials are never forwarded to api.split.io.
|
|
@@ -266,6 +309,8 @@ export class HarnessClient {
|
|
|
266
309
|
}
|
|
267
310
|
}
|
|
268
311
|
async request(options) {
|
|
312
|
+
if (options.responseType === "sse")
|
|
313
|
+
assertJsonEventStreamLimits(options.sseLimits);
|
|
269
314
|
await this.rateLimiter.acquire();
|
|
270
315
|
const method = options.method ?? "GET";
|
|
271
316
|
const url = this.buildUrl(options);
|
|
@@ -288,6 +333,7 @@ export class HarnessClient {
|
|
|
288
333
|
log.debug(`Retry attempt ${attempt}/${this.maxRetries}`, { backoffMs: Math.round(backoff) });
|
|
289
334
|
await new Promise((r) => setTimeout(r, backoff));
|
|
290
335
|
}
|
|
336
|
+
let timer;
|
|
291
337
|
try {
|
|
292
338
|
// Check if already aborted before starting the request
|
|
293
339
|
if (options.signal?.aborted) {
|
|
@@ -295,7 +341,7 @@ export class HarnessClient {
|
|
|
295
341
|
}
|
|
296
342
|
const timeoutController = new AbortController();
|
|
297
343
|
const effectiveTimeout = options.timeoutMs ?? this.timeout;
|
|
298
|
-
|
|
344
|
+
timer = setTimeout(() => timeoutController.abort(), effectiveTimeout);
|
|
299
345
|
// Merge external signal (client disconnect) with timeout signal
|
|
300
346
|
const signal = options.signal
|
|
301
347
|
? AbortSignal.any([options.signal, timeoutController.signal])
|
|
@@ -318,17 +364,12 @@ export class HarnessClient {
|
|
|
318
364
|
body: fetchBody,
|
|
319
365
|
signal,
|
|
320
366
|
});
|
|
321
|
-
|
|
367
|
+
// JSON/binary behavior is unchanged; SSE keeps the HTTP deadline through consumption.
|
|
368
|
+
if (options.responseType !== "sse")
|
|
369
|
+
clearTimeout(timer);
|
|
322
370
|
if (!response.ok) {
|
|
323
371
|
const body = await response.text();
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
parsed = JSON.parse(body);
|
|
327
|
-
}
|
|
328
|
-
catch {
|
|
329
|
-
// Non-JSON error (HTML proxy page, WAF block, etc.)
|
|
330
|
-
// Provide actionable messages instead of leaking raw HTML to the LLM
|
|
331
|
-
}
|
|
372
|
+
const parsed = parseApiError(body, options.path);
|
|
332
373
|
const rawMessage = isGarbageMessage(parsed.message)
|
|
333
374
|
? humanizeHttpError(response.status, body)
|
|
334
375
|
: parsed.message;
|
|
@@ -345,6 +386,18 @@ export class HarnessClient {
|
|
|
345
386
|
}
|
|
346
387
|
throw error;
|
|
347
388
|
}
|
|
389
|
+
if (options.responseType === "sse") {
|
|
390
|
+
try {
|
|
391
|
+
return await readJsonEventStream(response, signal, options.sseLimits);
|
|
392
|
+
}
|
|
393
|
+
catch (cause) {
|
|
394
|
+
// Never reconnect a consumed stream, including when the overall HTTP deadline expires.
|
|
395
|
+
if (timeoutController.signal.aborted && !options.signal?.aborted) {
|
|
396
|
+
throw new HarnessApiError("Request timed out", 408, undefined, undefined, cause);
|
|
397
|
+
}
|
|
398
|
+
throw cause;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
348
401
|
// Binary response mode — return raw ArrayBuffer (used for ZIP downloads)
|
|
349
402
|
if (options.responseType === "buffer") {
|
|
350
403
|
const buffer = await response.arrayBuffer();
|
|
@@ -385,6 +438,9 @@ export class HarnessClient {
|
|
|
385
438
|
}
|
|
386
439
|
throw new HarnessApiError(`Request failed: ${err.message ?? String(err)}`, 502, undefined, undefined, err);
|
|
387
440
|
}
|
|
441
|
+
finally {
|
|
442
|
+
clearTimeout(timer);
|
|
443
|
+
}
|
|
388
444
|
}
|
|
389
445
|
throw lastError ?? new HarnessApiError("Max retries exceeded", 500);
|
|
390
446
|
}
|
|
@@ -434,11 +490,7 @@ export class HarnessClient {
|
|
|
434
490
|
clearTimeout(timer);
|
|
435
491
|
if (!response.ok) {
|
|
436
492
|
const body = await response.text();
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
parsed = JSON.parse(body);
|
|
440
|
-
}
|
|
441
|
-
catch { /* non-JSON */ }
|
|
493
|
+
const parsed = parseApiError(body, options.path);
|
|
442
494
|
const rawMessage = isGarbageMessage(parsed.message)
|
|
443
495
|
? humanizeHttpError(response.status, body)
|
|
444
496
|
: parsed.message;
|