mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 02:00:52 +01:00
feat(apps): simplify PostHog connection setup
Default PostHog to broad tool access, move optional filters and response mode behind a reusable Advanced disclosure, and codify the connection UX rule in the bundled integration skill and playbook. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
cbd6c2b261
commit
860eb96c1e
12 files changed
+139
-65
No files matched your search
@@ -91,6 +91,11 @@ Capture:
|
||||
- `methods`: explicit combinations of `transport` (`mcp_remote`, `rest_api`, `local_stdio`), `authKind` (`oauth`, `api_key`, `none`), and `ownership` (`platform_shared`, `platform_provisioned`, `customer`, `dcr`).
|
||||
- Stable connection UID namespace used to form `{namespace}/{slug}` addresses.
|
||||
- `credentialFields`: labels, vendor-call placement, header key, prefix, help URL, and required state. User-facing labels should be sanitized by the Apps UI copy layer. The saved value is always a `company_secrets` ref, not an env entry.
|
||||
- `tenantFields` and `extensionFields`: keep unavoidable identity and resource
|
||||
boundaries in the default flow. Mark optional scope reduction, feature/tool
|
||||
filters, response modes, and transport tuning with `advanced: true`, and give
|
||||
advanced fields working defaults that do not require the operator to expand
|
||||
the disclosure.
|
||||
- `oauth`: provider key, scopes, authorization URL, token URL, metadata URL if applicable.
|
||||
- `urlPatterns`: URLs that can identify this app during paste/import flows.
|
||||
- `recommendedDefaults`: access and risk defaults, especially ask-first risk levels.
|
||||
@@ -151,6 +156,13 @@ The wizard path comes from auth mode and transport:
|
||||
|
||||
The operator should see Apps, Connections, and Review language. Keep protocol language behind Developer/Advanced copy.
|
||||
|
||||
Default to the broadest vendor permissions and scopes the reviewed connection
|
||||
can support. Operators should not have to predict every future tool during
|
||||
setup. Keep the default view to the minimum inputs needed for a working
|
||||
connection, fold optional expert controls under one collapsed **Advanced**
|
||||
disclosure, and enforce safe execution afterward through Paperclip's resource
|
||||
boundaries, catalog review, ask-first policies, quarantine, and audit.
|
||||
|
||||
## Step 8: Apply Governance Defaults
|
||||
|
||||
Governance is automatic because every catalog entry becomes a normal tool-access object:
|
||||
@@ -750,4 +762,3 @@ End-to-end evidence belongs to PAP-16654 (P6) and the PAP-12373 matrix:
|
||||
- Revocation removes Notion tools and blocks execution.
|
||||
- Audit rows prove actor, run/issue context, connection, tool, decision,
|
||||
reason code, and outcome.
|
||||
|
||||
+14
-11
@@ -24,7 +24,7 @@ sequenceDiagram
|
||||
participant M as mcp.posthog.com
|
||||
participant O as oauth.posthog.com
|
||||
|
||||
A->>P: Choose PostHog sign-in and project filters
|
||||
A->>P: Choose PostHog sign-in and project
|
||||
P->>M: Discover protected-resource metadata
|
||||
M-->>P: Authorization server metadata URL
|
||||
P->>O: Discover endpoints and register OAuth client
|
||||
@@ -34,8 +34,8 @@ sequenceDiagram
|
||||
O-->>P: Redirect to /api/tools/oauth/callback
|
||||
P->>O: Exchange authorization code
|
||||
O-->>P: Access and refresh tokens
|
||||
P->>M: tools/list with project and safety filters
|
||||
M-->>P: Filtered PostHog tool catalog
|
||||
P->>M: tools/list with project and configured options
|
||||
M-->>P: PostHog tool catalog
|
||||
```
|
||||
|
||||
The current hosted endpoints are:
|
||||
@@ -60,11 +60,13 @@ values for either.
|
||||
1. In **Apps → Browse**, choose **PostHog**.
|
||||
2. Explicitly choose **Sign in with PostHog** or **Use a personal API key**.
|
||||
3. Enter the numeric PostHog project ID.
|
||||
4. Keep **Read-only mode** enabled unless agents have an approved need to make
|
||||
changes.
|
||||
5. Limit the catalog with one or both of **Feature groups** and **Individual
|
||||
tools**. Paperclip currently fixes response mode to individual tools; CLI
|
||||
mode is unavailable until nested execution is governed.
|
||||
4. Leave **Read-only mode** off to expose the full PostHog action catalog.
|
||||
Turn it on when the connection should never offer actions that change data.
|
||||
5. The default setup requests all feature groups and tools. Open **Advanced**
|
||||
only to narrow the catalog with **Feature groups** or **Individual tools**.
|
||||
Paperclip fixes the advanced response mode to individual tools so each
|
||||
action can be governed; CLI mode is unavailable until nested execution is
|
||||
governed.
|
||||
6. For OAuth, continue through browser consent. For API-key setup, create a
|
||||
personal API key using PostHog's **MCP Server** preset and paste it into
|
||||
Paperclip. Never put the key in connection configuration or a URL.
|
||||
@@ -73,9 +75,10 @@ values for either.
|
||||
write risk until reviewed.
|
||||
|
||||
Paperclip sends the project scope as the `x-posthog-project-id` managed header.
|
||||
It sends `readonly`, `features`, `tools`, and `mode` as query parameters. The
|
||||
managed header is identical during catalog discovery and tool execution, and a
|
||||
caller cannot override it. PostHog documents these options in its [MCP
|
||||
It sends configured `readonly`, `features`, `tools`, and `mode` values as query
|
||||
parameters. Leaving the optional feature and tool filters blank exposes the
|
||||
full catalog. The managed header is identical during catalog discovery and tool
|
||||
execution, and a caller cannot override it. PostHog documents these options in its [MCP
|
||||
overview](https://posthog.com/docs/model-context-protocol) and [MCP
|
||||
FAQ](https://posthog.com/docs/model-context-protocol/faq).
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@ describe("AppDefinition catalog",()=>{
|
||||
expect(notion?.methods[0]?.defaults).toEqual({serverUrl:"https://mcp.notion.com/mcp"});
|
||||
});
|
||||
it("preserves required Linear OAuth scopes",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="linear")?.methods[0]?.defaults?.scopesHint).toEqual(["read","write"]));
|
||||
it("offers PostHog OAuth and API-key methods with pinned safe defaults",()=>{const posthog=APP_DEFINITIONS.find((app)=>app.slug==="posthog");expect(posthog?.methods.map((method)=>method.key)).toEqual(["mcp-oauth","mcp-api-key"]);for(const method of posthog?.methods??[]){expect(method.riskTier).toBe("S3");expect(method.tenantFields?.find((field)=>field.key==="readOnly")?.defaultValue).toBe(true);expect(method.tenantFields?.find((field)=>field.key==="projectId")?.transport).toEqual({location:"header",name:"x-posthog-project-id"});expect(method.configRequirements?.atLeastOneOf).toEqual(["features","tools"])}});
|
||||
it("offers PostHog OAuth and API-key methods with broad defaults and advanced narrowing",()=>{const posthog=APP_DEFINITIONS.find((app)=>app.slug==="posthog");expect(posthog?.methods.map((method)=>method.key)).toEqual(["mcp-oauth","mcp-api-key"]);for(const method of posthog?.methods??[]){expect(method.riskTier).toBe("S3");expect(method.tenantFields?.find((field)=>field.key==="readOnly")?.defaultValue).toBe(false);expect(method.tenantFields?.find((field)=>field.key==="projectId")?.transport).toEqual({location:"header",name:"x-posthog-project-id"});expect(method.tenantFields?.filter((field)=>field.advanced).map((field)=>field.key)).toEqual(["features","tools","mode"]);expect(method.configRequirements).toBeUndefined();expect(method.requiredResourceFilters).toEqual(["project"])}});
|
||||
it("enforces method and field invariants",()=>{for(const app of APP_DEFINITIONS)for(const method of app.methods){if(method.auth==="api_key")expect(method.keyPlacement).toBeTruthy();if(method.auth==="oauth")expect(method.ownershipModes.length).toBeGreaterThan(0);for(const field of method.credentialFields??[])if(field.required&&field.type!=="checkbox")expect(field.placeholder).toBeTruthy()}});
|
||||
});
|
||||
@@ -26,7 +26,7 @@
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.posthog.com/mcp"
|
||||
},
|
||||
"guidanceMd": "Pin the connection to one PostHog project, keep read-only mode enabled by default, and expose only the feature groups or tools agents need.",
|
||||
"guidanceMd": "Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.",
|
||||
"riskTier": "S3",
|
||||
"tenantFields": [
|
||||
{
|
||||
@@ -49,8 +49,8 @@
|
||||
"key": "readOnly",
|
||||
"label": "Read-only mode",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"helperMd": "Keep enabled unless agents explicitly need to change PostHog data.",
|
||||
"defaultValue": false,
|
||||
"helperMd": "Turn on to hide tools that can change PostHog data.",
|
||||
"transport": {
|
||||
"location": "query",
|
||||
"name": "readonly",
|
||||
@@ -62,9 +62,9 @@
|
||||
"key": "features",
|
||||
"label": "Feature groups",
|
||||
"type": "textarea",
|
||||
"placeholder": "insights, error_tracking, flags, experiments",
|
||||
"defaultValue": "insights,error_tracking,flags,experiments",
|
||||
"helperMd": "Comma-separated PostHog feature groups to expose.",
|
||||
"advanced": true,
|
||||
"placeholder": "Optional comma-separated feature groups",
|
||||
"helperMd": "Leave blank to expose every feature group, or enter a comma-separated list to narrow access.",
|
||||
"validation": {
|
||||
"maxLength": 500
|
||||
},
|
||||
@@ -78,8 +78,9 @@
|
||||
"key": "tools",
|
||||
"label": "Individual tools",
|
||||
"type": "textarea",
|
||||
"advanced": true,
|
||||
"placeholder": "Optional comma-separated tool names",
|
||||
"helperMd": "Optional exact tool names. These are combined with feature groups.",
|
||||
"helperMd": "Leave blank to expose all tools. Exact names here are combined with any feature groups.",
|
||||
"validation": {
|
||||
"maxLength": 2000
|
||||
},
|
||||
@@ -93,6 +94,7 @@
|
||||
"key": "mode",
|
||||
"label": "Tool response mode",
|
||||
"type": "select",
|
||||
"advanced": true,
|
||||
"required": true,
|
||||
"placeholder": "Individual tools",
|
||||
"defaultValue": "tools",
|
||||
@@ -102,25 +104,15 @@
|
||||
"label": "Individual tools"
|
||||
}
|
||||
],
|
||||
"helperMd": "CLI mode remains unavailable until nested execution is governed.",
|
||||
"helperMd": "Paperclip uses individual tools so every action can be governed. CLI mode remains unavailable until nested execution is governed.",
|
||||
"transport": {
|
||||
"location": "query",
|
||||
"name": "mode"
|
||||
}
|
||||
}
|
||||
],
|
||||
"configRequirements": {
|
||||
"atLeastOneOf": [
|
||||
"features",
|
||||
"tools"
|
||||
]
|
||||
},
|
||||
"requiredResourceFilters": [
|
||||
"project",
|
||||
"read_only",
|
||||
"features",
|
||||
"tools",
|
||||
"mode"
|
||||
"project"
|
||||
],
|
||||
"label": "Sign in with PostHog",
|
||||
"consoleLinks": {
|
||||
@@ -138,7 +130,7 @@
|
||||
"defaults": {
|
||||
"serverUrl": "https://mcp.posthog.com/mcp"
|
||||
},
|
||||
"guidanceMd": "Pin the connection to one PostHog project, keep read-only mode enabled by default, and expose only the feature groups or tools agents need.",
|
||||
"guidanceMd": "Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.",
|
||||
"riskTier": "S3",
|
||||
"tenantFields": [
|
||||
{
|
||||
@@ -161,8 +153,8 @@
|
||||
"key": "readOnly",
|
||||
"label": "Read-only mode",
|
||||
"type": "checkbox",
|
||||
"defaultValue": true,
|
||||
"helperMd": "Keep enabled unless agents explicitly need to change PostHog data.",
|
||||
"defaultValue": false,
|
||||
"helperMd": "Turn on to hide tools that can change PostHog data.",
|
||||
"transport": {
|
||||
"location": "query",
|
||||
"name": "readonly",
|
||||
@@ -174,9 +166,9 @@
|
||||
"key": "features",
|
||||
"label": "Feature groups",
|
||||
"type": "textarea",
|
||||
"placeholder": "insights, error_tracking, flags, experiments",
|
||||
"defaultValue": "insights,error_tracking,flags,experiments",
|
||||
"helperMd": "Comma-separated PostHog feature groups to expose.",
|
||||
"advanced": true,
|
||||
"placeholder": "Optional comma-separated feature groups",
|
||||
"helperMd": "Leave blank to expose every feature group, or enter a comma-separated list to narrow access.",
|
||||
"validation": {
|
||||
"maxLength": 500
|
||||
},
|
||||
@@ -190,8 +182,9 @@
|
||||
"key": "tools",
|
||||
"label": "Individual tools",
|
||||
"type": "textarea",
|
||||
"advanced": true,
|
||||
"placeholder": "Optional comma-separated tool names",
|
||||
"helperMd": "Optional exact tool names. These are combined with feature groups.",
|
||||
"helperMd": "Leave blank to expose all tools. Exact names here are combined with any feature groups.",
|
||||
"validation": {
|
||||
"maxLength": 2000
|
||||
},
|
||||
@@ -205,6 +198,7 @@
|
||||
"key": "mode",
|
||||
"label": "Tool response mode",
|
||||
"type": "select",
|
||||
"advanced": true,
|
||||
"required": true,
|
||||
"placeholder": "Individual tools",
|
||||
"defaultValue": "tools",
|
||||
@@ -214,25 +208,15 @@
|
||||
"label": "Individual tools"
|
||||
}
|
||||
],
|
||||
"helperMd": "CLI mode remains unavailable until nested execution is governed.",
|
||||
"helperMd": "Paperclip uses individual tools so every action can be governed. CLI mode remains unavailable until nested execution is governed.",
|
||||
"transport": {
|
||||
"location": "query",
|
||||
"name": "mode"
|
||||
}
|
||||
}
|
||||
],
|
||||
"configRequirements": {
|
||||
"atLeastOneOf": [
|
||||
"features",
|
||||
"tools"
|
||||
]
|
||||
},
|
||||
"requiredResourceFilters": [
|
||||
"project",
|
||||
"read_only",
|
||||
"features",
|
||||
"tools",
|
||||
"mode"
|
||||
"project"
|
||||
],
|
||||
"label": "Use a personal API key",
|
||||
"credentialFields": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ToolConnectionOwnership, ToolConnectionTransport } from "./tool-access.js";
|
||||
export type AppCategory = "ai"|"analytics"|"commerce"|"communication"|"content"|"data"|"developer"|"productivity"|"other";
|
||||
export type OAuthRedirectConstraints = "https-or-loopback-http";
|
||||
export interface FieldDef { key:string; label:string; type:"text"|"password"|"textarea"|"datetime"|"select"|"checkbox"; required?:boolean; placeholder?:string; helperMd?:string; secret?:boolean; prefix?:string; defaultValue?:string|boolean; validation?:{pattern?:string;maxLength?:number}; options?:Array<{value:string;label:string}>; transport?:{location:"query"|"header";name:string;format?:"string"|"csv"|"boolean";omitFalse?:boolean} }
|
||||
export interface FieldDef { key:string; label:string; type:"text"|"password"|"textarea"|"datetime"|"select"|"checkbox"; required?:boolean; advanced?:boolean; placeholder?:string; helperMd?:string; secret?:boolean; prefix?:string; defaultValue?:string|boolean; validation?:{pattern?:string;maxLength?:number}; options?:Array<{value:string;label:string}>; transport?:{location:"query"|"header";name:string;format?:"string"|"csv"|"boolean";omitFalse?:boolean} }
|
||||
export interface ConnectionMethodDef { key:string; label?:string; transport:ToolConnectionTransport; auth:"oauth"|"api_key"|"none"; ownershipModes:ToolConnectionOwnership[]; whenToUse:string; defaults?:{serverUrl?:string;discoveryUrl?:string|null;serviceHost?:string;templateKey?:string;authorizationEndpoint?:string;tokenEndpoint?:string;metadataUrl?:string;scopesHint?:string[]}; tenantFields?:FieldDef[]; extensionFields?:FieldDef[]; configRequirements?:{atLeastOneOf?:string[]}; credentialFields?:FieldDef[]; keyPlacement?:{location:"header"|"query"|"body_json"|"env";name:string;prefix?:string|null}; guidanceMd:string; consoleLinks?:{register?:string;keys?:string;settings?:string;docs?:string}; warnings?:string[]; variants?:Array<{key:string;label:string;whenToUse:string;tenantFields?:FieldDef[]}>; riskTier:"S1"|"S2"|"S3"|"S4"; requiredResourceFilters?:string[] }
|
||||
export interface AppDefinition { schemaVersion:1; slug:string; name:string; description:string; categories:AppCategory[]; featured?:boolean; branding:{logoUrl:string;darkLogoUrl?:string;backgroundColor?:string;accentColor?:string}; urlPatterns:string[]; docsUrl?:string; redirectConstraints?:OAuthRedirectConstraints; methods:ConnectionMethodDef[]; suggestable?:boolean; availability?:{available:boolean;reason?:string;robotEmail?:string}; ownershipAvailability?:Partial<Record<ToolConnectionOwnership,boolean>> }
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { toolConnectionOwnershipSchema, toolConnectionTransportSchema } from "./tool-access.js";
|
||||
const field=z.object({key:z.string().min(1),label:z.string().min(1),type:z.enum(["text","password","textarea","datetime","select","checkbox"]),required:z.boolean().optional(),placeholder:z.string().optional(),helperMd:z.string().optional(),secret:z.boolean().optional(),prefix:z.string().optional(),defaultValue:z.union([z.string(),z.boolean()]).optional(),validation:z.object({pattern:z.string().optional(),maxLength:z.number().int().positive().optional()}).optional(),options:z.array(z.object({value:z.string(),label:z.string()})).optional(),transport:z.object({location:z.enum(["query","header"]),name:z.string().min(1),format:z.enum(["string","csv","boolean"]).optional(),omitFalse:z.boolean().optional()}).optional()}).superRefine((v,c)=>{if(v.required&&v.type!=="checkbox"&&!v.placeholder)c.addIssue({code:"custom",message:"Required fields need placeholders",path:["placeholder"]});if(v.type==="select"&&(!v.options||v.options.length===0))c.addIssue({code:"custom",message:"Select fields need options",path:["options"]})});
|
||||
const field=z.object({key:z.string().min(1),label:z.string().min(1),type:z.enum(["text","password","textarea","datetime","select","checkbox"]),required:z.boolean().optional(),advanced:z.boolean().optional(),placeholder:z.string().optional(),helperMd:z.string().optional(),secret:z.boolean().optional(),prefix:z.string().optional(),defaultValue:z.union([z.string(),z.boolean()]).optional(),validation:z.object({pattern:z.string().optional(),maxLength:z.number().int().positive().optional()}).optional(),options:z.array(z.object({value:z.string(),label:z.string()})).optional(),transport:z.object({location:z.enum(["query","header"]),name:z.string().min(1),format:z.enum(["string","csv","boolean"]).optional(),omitFalse:z.boolean().optional()}).optional()}).superRefine((v,c)=>{if(v.required&&v.type!=="checkbox"&&!v.placeholder)c.addIssue({code:"custom",message:"Required fields need placeholders",path:["placeholder"]});if(v.type==="select"&&(!v.options||v.options.length===0))c.addIssue({code:"custom",message:"Select fields need options",path:["options"]})});
|
||||
export const connectionMethodDefSchema=z.object({key:z.string().min(1),label:z.string().min(1).optional(),transport:toolConnectionTransportSchema,auth:z.enum(["oauth","api_key","none"]),ownershipModes:z.array(toolConnectionOwnershipSchema).min(1),whenToUse:z.string().min(1),defaults:z.object({serverUrl:z.string().url().optional(),discoveryUrl:z.string().url().nullable().optional(),serviceHost:z.string().optional(),templateKey:z.string().optional(),authorizationEndpoint:z.string().url().optional(),tokenEndpoint:z.string().url().optional(),metadataUrl:z.string().url().optional(),scopesHint:z.array(z.string()).optional()}).optional(),tenantFields:z.array(field).optional(),extensionFields:z.array(field).optional(),configRequirements:z.object({atLeastOneOf:z.array(z.string().min(1)).min(1).optional()}).optional(),credentialFields:z.array(field).optional(),keyPlacement:z.object({location:z.enum(["header","query","body_json","env"]),name:z.string().min(1),prefix:z.string().nullable().optional()}).optional(),guidanceMd:z.string().min(1),consoleLinks:z.object({register:z.string().url().optional(),keys:z.string().url().optional(),settings:z.string().url().optional(),docs:z.string().url().optional()}).optional(),warnings:z.array(z.string()).optional(),variants:z.array(z.object({key:z.string(),label:z.string(),whenToUse:z.string(),tenantFields:z.array(field).optional()})).optional(),riskTier:z.enum(["S1","S2","S3","S4"]),requiredResourceFilters:z.array(z.string()).optional()}).superRefine((v,c)=>{if(v.auth==="api_key"&&!v.keyPlacement)c.addIssue({code:"custom",message:"API-key methods require keyPlacement",path:["keyPlacement"]});const keys=new Set([...(v.tenantFields??[]),...(v.extensionFields??[])].map((entry)=>entry.key));for(const key of v.configRequirements?.atLeastOneOf??[])if(!keys.has(key))c.addIssue({code:"custom",message:"Config requirement references an unknown field",path:["configRequirements","atLeastOneOf"]})});
|
||||
export const appDefinitionSchema=z.object({schemaVersion:z.literal(1),slug:z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),name:z.string().min(1),description:z.string().min(1),categories:z.array(z.enum(["ai","analytics","commerce","communication","content","data","developer","productivity","other"])).min(1),featured:z.boolean().optional(),branding:z.object({logoUrl:z.string().url(),darkLogoUrl:z.string().url().optional(),backgroundColor:z.string().optional(),accentColor:z.string().optional()}),urlPatterns:z.array(z.string()),docsUrl:z.string().url().optional(),redirectConstraints:z.enum(["https-or-loopback-http"]).optional(),methods:z.array(connectionMethodDefSchema).min(1),suggestable:z.boolean().optional(),availability:z.object({available:z.boolean(),reason:z.string().optional(),robotEmail:z.string().optional()}).optional(),ownershipAvailability:z.object({platform_shared:z.boolean().optional(),platform_provisioned:z.boolean().optional(),customer:z.boolean().optional(),dcr:z.boolean().optional()}).optional()});
|
||||
export const appDefinitionsSchema=z.array(appDefinitionSchema).superRefine((v,c)=>{const s=new Set<string>();v.forEach((a,i)=>{if(s.has(a.slug))c.addIssue({code:"custom",message:"Duplicate slug",path:[i,"slug"]});s.add(a.slug)})});
|
||||
+20
@@ -49,6 +49,26 @@ App only after a human accepts the exact research revision and connection set.
|
||||
briefs, catalog files, issue text, plans, fixtures, screenshots, logs, branch
|
||||
names, commits, or PRs.
|
||||
|
||||
## Default Connection Experience
|
||||
|
||||
- Request the broadest vendor permissions and scopes the connection can
|
||||
support by default. Operators should not have to predict every future tool
|
||||
they may need during setup. Enforce safe use after connection through
|
||||
Paperclip's action catalog, resource boundaries, ask-first policies,
|
||||
quarantine, and audit controls.
|
||||
- Keep the default wizard limited to the minimum information needed to create
|
||||
a working connection: connection identity, authentication, and any
|
||||
unavoidable tenant or resource boundary. Put optional scope reduction,
|
||||
feature groups, individual tool filters, response modes, transport tuning,
|
||||
and other expert controls behind one collapsed **Advanced** disclosure.
|
||||
- Give advanced controls working broad defaults so an operator can finish
|
||||
setup without opening them. When a provider truly requires an explicit
|
||||
advanced choice, document the exception and explain it in plain language
|
||||
instead of exposing protocol details by default.
|
||||
- Treat vendor permission breadth and Paperclip execution governance as
|
||||
separate layers. Do not reduce requested vendor permissions merely to stand
|
||||
in for missing action review, approval, quarantine, or audit policy.
|
||||
|
||||
## Preflight
|
||||
|
||||
1. Load the current Paperclip skill for checkout, comments, interactions,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"packageName": "@paperclipai/skills-catalog",
|
||||
"packageVersion": "0.3.1",
|
||||
"generatedAt": "2026-08-07T20:30:29.973Z",
|
||||
"generatedAt": "2026-08-11T15:05:34.526Z",
|
||||
"skills": [
|
||||
{
|
||||
"id": "paperclipai:bundled:docs:doc-maintenance",
|
||||
@@ -1149,8 +1149,8 @@
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "skill",
|
||||
"sizeBytes": 11183,
|
||||
"sha256": "452386be3aa5e2988dc9d72f288f469975da766626a25c48017877963c533781"
|
||||
"sizeBytes": 12409,
|
||||
"sha256": "5278d288a0cbfe10fdda4ef3d99cf61e6ed068f4b6c1282406b0e001c9f45253"
|
||||
},
|
||||
{
|
||||
"path": "examples/notion-mcp-research-gate.md",
|
||||
@@ -1159,7 +1159,7 @@
|
||||
"sha256": "944b13ac67c06c65643eb236e856f1479106749861535648d29334f84cf1b50b"
|
||||
}
|
||||
],
|
||||
"contentHash": "sha256:4dbb76624e0fd5502fa10a9168f77214c5020899838af887e77a1617a0a00c31"
|
||||
"contentHash": "sha256:84f2a28d7d004393719684f2b435e5ee01ddc58591e0dfeb73d72c7605db7f49"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,12 +5,12 @@ const field=(key,label,placeholder)=>({key,label,type:"password",required:true,p
|
||||
const method=(key,transport,auth,defaults,riskTier,guidanceMd,extra={})=>({key,transport,auth,ownershipModes:auth==="oauth"?["customer","dcr"]:["customer"],whenToUse:transport==="mcp_remote"?"Use the provider-hosted connection for the quickest setup.":"Use credentials from your provider account.",defaults,guidanceMd,riskTier,...extra});
|
||||
const posthogConfigFields=()=>[
|
||||
{key:"projectId",label:"Project ID",type:"text",required:true,placeholder:"12345",helperMd:"Find the numeric project ID in PostHog project settings.",validation:{pattern:"^[0-9]+$",maxLength:32},transport:{location:"header",name:"x-posthog-project-id"}},
|
||||
{key:"readOnly",label:"Read-only mode",type:"checkbox",defaultValue:true,helperMd:"Keep enabled unless agents explicitly need to change PostHog data.",transport:{location:"query",name:"readonly",format:"boolean",omitFalse:true}},
|
||||
{key:"features",label:"Feature groups",type:"textarea",placeholder:"insights, error_tracking, flags, experiments",defaultValue:"insights,error_tracking,flags,experiments",helperMd:"Comma-separated PostHog feature groups to expose.",validation:{maxLength:500},transport:{location:"query",name:"features",format:"csv"}},
|
||||
{key:"tools",label:"Individual tools",type:"textarea",placeholder:"Optional comma-separated tool names",helperMd:"Optional exact tool names. These are combined with feature groups.",validation:{maxLength:2000},transport:{location:"query",name:"tools",format:"csv"}},
|
||||
{key:"mode",label:"Tool response mode",type:"select",required:true,placeholder:"Individual tools",defaultValue:"tools",options:[{value:"tools",label:"Individual tools"}],helperMd:"CLI mode remains unavailable until nested execution is governed.",transport:{location:"query",name:"mode"}},
|
||||
{key:"readOnly",label:"Read-only mode",type:"checkbox",defaultValue:false,helperMd:"Turn on to hide tools that can change PostHog data.",transport:{location:"query",name:"readonly",format:"boolean",omitFalse:true}},
|
||||
{key:"features",label:"Feature groups",type:"textarea",advanced:true,placeholder:"Optional comma-separated feature groups",helperMd:"Leave blank to expose every feature group, or enter a comma-separated list to narrow access.",validation:{maxLength:500},transport:{location:"query",name:"features",format:"csv"}},
|
||||
{key:"tools",label:"Individual tools",type:"textarea",advanced:true,placeholder:"Optional comma-separated tool names",helperMd:"Leave blank to expose all tools. Exact names here are combined with any feature groups.",validation:{maxLength:2000},transport:{location:"query",name:"tools",format:"csv"}},
|
||||
{key:"mode",label:"Tool response mode",type:"select",advanced:true,required:true,placeholder:"Individual tools",defaultValue:"tools",options:[{value:"tools",label:"Individual tools"}],helperMd:"Paperclip uses individual tools so every action can be governed. CLI mode remains unavailable until nested execution is governed.",transport:{location:"query",name:"mode"}},
|
||||
];
|
||||
const posthogMethod=(key,auth,extra={})=>method(key,"mcp_remote",auth,{serverUrl:"https://mcp.posthog.com/mcp"},"S3","Pin the connection to one PostHog project, keep read-only mode enabled by default, and expose only the feature groups or tools agents need.",{tenantFields:posthogConfigFields(),configRequirements:{atLeastOneOf:["features","tools"]},requiredResourceFilters:["project","read_only","features","tools","mode"],...extra});
|
||||
const posthogMethod=(key,auth,extra={})=>method(key,"mcp_remote",auth,{serverUrl:"https://mcp.posthog.com/mcp"},"S3","Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.",{tenantFields:posthogConfigFields(),requiredResourceFilters:["project"],...extra});
|
||||
const apps=[
|
||||
["zapier","Zapier","Reach thousands of apps through your Zapier account.","productivity","zapier.com",["https://mcp.zapier.com/*"],method("mcp-key","mcp_remote","api_key",{serverUrl:"https://mcp.zapier.com/api/mcp"},"S3","Create a Zapier MCP connection, then paste its token here.",{credentialFields:[field("authorization","Zapier MCP token","Paste your Zapier token")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "}})],
|
||||
["github","GitHub","Read code and pull requests, and coordinate repository work.","developer","github.com",["https://api.githubcopilot.com/mcp/*"],method("mcp-key","mcp_remote","api_key",{serverUrl:"https://api.githubcopilot.com/mcp/"},"S3","Create a fine-grained token limited to the repositories agents should use.",{credentialFields:[field("authorization","GitHub token","github_pat_...")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "},requiredResourceFilters:["organization","repository"]})],
|
||||
|
||||
@@ -7172,6 +7172,20 @@ describe("normalizeConnectionMethodConfig", () => {
|
||||
const posthog = getConnectableAppDefinition("posthog")!;
|
||||
const apiKeyMethod = posthog.methods.find((method) => method.key === "mcp-api-key")!;
|
||||
|
||||
it("uses the broad PostHog catalog when optional advanced filters are untouched", () => {
|
||||
expect(normalizeConnectionMethodConfig(apiKeyMethod, {
|
||||
projectId: "12345",
|
||||
})).toEqual({
|
||||
values: {
|
||||
projectId: "12345",
|
||||
readOnly: false,
|
||||
mode: "tools",
|
||||
},
|
||||
url: "https://mcp.posthog.com/mcp?mode=tools",
|
||||
headers: { "x-posthog-project-id": "12345" },
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes and projects PostHog scope without accepting arbitrary config", () => {
|
||||
expect(normalizeConnectionMethodConfig(apiKeyMethod, {
|
||||
projectId: "12345",
|
||||
|
||||
@@ -229,8 +229,25 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
|
||||
|
||||
const projectInput = container.querySelector<HTMLInputElement>('input[placeholder="12345"]');
|
||||
const keyInput = container.querySelector<HTMLInputElement>('input[type="password"]');
|
||||
const advanced = buttonByText("Advanced");
|
||||
expect(projectInput).toBeTruthy();
|
||||
expect(keyInput).toBeTruthy();
|
||||
expect(container.querySelector('[role="switch"]')?.getAttribute("aria-checked")).toBe("false");
|
||||
expect(advanced?.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(container.textContent).not.toContain("Feature groups");
|
||||
expect(container.textContent).not.toContain("Individual tools");
|
||||
expect(container.textContent).not.toContain("Tool response mode");
|
||||
|
||||
await act(async () => {
|
||||
advanced?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
expect(advanced?.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(container.textContent).toContain("Feature groups");
|
||||
expect(container.textContent).toContain("Individual tools");
|
||||
expect(container.textContent).toContain("Tool response mode");
|
||||
|
||||
await act(async () => {
|
||||
setInputValue(projectInput!, "12345");
|
||||
setInputValue(keyInput!, "phx_test-key");
|
||||
@@ -248,8 +265,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
|
||||
credentialValues: { "credentials.authorization": "phx_test-key" },
|
||||
configValues: {
|
||||
projectId: "12345",
|
||||
readOnly: true,
|
||||
features: "insights,error_tracking,flags,experiments",
|
||||
readOnly: false,
|
||||
mode: "tools",
|
||||
},
|
||||
applicationId: undefined,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
ClipboardPaste,
|
||||
@@ -38,6 +39,7 @@ import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { AgentMultiSelect } from "@/components/AgentMultiSelect";
|
||||
import { InlineBanner } from "@/components/InlineBanner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
||||
@@ -1417,6 +1419,9 @@ function KeyStep({
|
||||
(f) => f.required === false || (values[f.configPath]?.trim().length ?? 0) > 0,
|
||||
);
|
||||
const configFields = [...(method?.tenantFields ?? []), ...(method?.extensionFields ?? [])];
|
||||
const standardConfigFields = configFields.filter((field) => field.advanced !== true);
|
||||
const advancedConfigFields = configFields.filter((field) => field.advanced === true);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const configFilled = configFields.every((field) => {
|
||||
if (!field.required) return true;
|
||||
const value = configValues[field.key];
|
||||
@@ -1537,7 +1542,7 @@ function KeyStep({
|
||||
|
||||
<ConnectionNameField name={name} onNameChange={onNameChange} />
|
||||
|
||||
{configFields.map((field) => (
|
||||
{standardConfigFields.map((field) => (
|
||||
<MethodConfigField
|
||||
key={field.key}
|
||||
field={field}
|
||||
@@ -1546,6 +1551,27 @@ function KeyStep({
|
||||
/>
|
||||
))}
|
||||
|
||||
{advancedConfigFields.length > 0 && (
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground">
|
||||
<ChevronDown className={cn("h-4 w-4 transition-transform", advancedOpen && "rotate-180")} />
|
||||
Advanced
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-4">
|
||||
<div className="space-y-6">
|
||||
{advancedConfigFields.map((field) => (
|
||||
<MethodConfigField
|
||||
key={field.key}
|
||||
field={field}
|
||||
value={configValues[field.key]}
|
||||
onChange={(value) => onConfigChange({ ...configValues, [field.key]: value })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
|
||||
{method && fields.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{method.auth === "oauth"
|
||||
|
||||
Reference in new issue
Block a user