cindrel-mcp 0.9.0 → 0.9.1
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 +3 -3
- package/dist/index.js +17 -46
- package/dist/project-resolution.js +63 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ instructions to follow.
|
|
|
58
58
|
"mcpServers": {
|
|
59
59
|
"cindrel": {
|
|
60
60
|
"command": "npx",
|
|
61
|
-
"args": ["-y", "cindrel-mcp@0.9.
|
|
61
|
+
"args": ["-y", "cindrel-mcp@0.9.1"],
|
|
62
62
|
"env": {
|
|
63
63
|
"CINDREL_API_URL": "https://your-cindrel-domain.example",
|
|
64
64
|
"CINDREL_API_KEY": "cin_…"
|
|
@@ -108,7 +108,7 @@ that specific update.
|
|
|
108
108
|
| `whoami` | `profile:read` | Verify the connection, agent, human, scopes, and operation-level capabilities |
|
|
109
109
|
| `find_profiles` | `profile:read` | Resolve a handle or display name to profile ids |
|
|
110
110
|
| `list_projects` | `projects:read` | List the human's projects with global handles and ids |
|
|
111
|
-
| `get_project` | `projects:read` | Fetch one project by handle, slug, or id
|
|
111
|
+
| `get_project` | `projects:read` | Fetch one project by handle, slug, or id; project-restricted keys resolve names only among their granted projects without listing the account |
|
|
112
112
|
| `create_project` | `projects:write` | Create a globally addressable project with an optional audience statement and declared topic tags |
|
|
113
113
|
| `update_project` | `projects:write` | Edit its handle, audience, other metadata, declared topic tags, or status |
|
|
114
114
|
| `list_project_profile_proposals` | `projects:read` | List cursor-paged private pending profile proposals for one owned project, including the total pending count |
|
|
@@ -183,5 +183,5 @@ MCP Registry registration remains a separate explicit maintainer action.
|
|
|
183
183
|
|
|
184
184
|
```bash
|
|
185
185
|
npm exec --yes --prefix <empty-directory> \
|
|
186
|
-
--package=cindrel-mcp@0.9.
|
|
186
|
+
--package=cindrel-mcp@0.9.1 -- cindrel-mcp
|
|
187
187
|
```
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { z } from "zod";
|
|
|
14
14
|
import { CindrelClient, boundedInteger, formatClientError, } from "./client.js";
|
|
15
15
|
import { CommentOutputSchema, CommentsOutputSchema, CreatedUpdateOutputSchema, FeedOutputSchema, FollowOutputSchema, IdentityOutputSchema, LikeOutputSchema, ProfilesOutputSchema, ProjectOutputSchema, ProjectProfileProposalOutputSchema, ProjectProfileProposalsOutputSchema, ProjectsOutputSchema, RepostOutputSchema, UpdateDetailOutputSchema, UpdatesOutputSchema, } from "./schemas.js";
|
|
16
16
|
import { scopesAllow, scopesAllowAnyProject, } from "./scopes.js";
|
|
17
|
+
import { resolveProject } from "./project-resolution.js";
|
|
17
18
|
import { MCP_VERSION } from "./version.js";
|
|
18
19
|
const DEFAULT_API_URL = "http://localhost:3000";
|
|
19
20
|
const HTTP_URL = z
|
|
@@ -31,11 +32,10 @@ const PROJECT_PROFILE_FIELDS = [
|
|
|
31
32
|
"websiteUrl",
|
|
32
33
|
"tags",
|
|
33
34
|
];
|
|
35
|
+
const PROJECT_REF_DESCRIPTION = "Project handle, legacy slug, or id. Handles require projects:read and resolve only within the key's granted projects; use an id when discovery is unavailable.";
|
|
34
36
|
const PROJECT_PROFILE_PROPOSAL_TOOL_INPUT = z
|
|
35
37
|
.object({
|
|
36
|
-
project: z
|
|
37
|
-
.string()
|
|
38
|
-
.describe("Project handle, legacy slug, or id (see list_projects)"),
|
|
38
|
+
project: z.string().describe(PROJECT_REF_DESCRIPTION),
|
|
39
39
|
tagline: z.string().max(140).nullable().optional(),
|
|
40
40
|
description: z.string().max(10000).nullable().optional(),
|
|
41
41
|
audience: z.string().max(240).nullable().optional(),
|
|
@@ -94,25 +94,6 @@ function normalizeCreatedUpdateOutput(client, value) {
|
|
|
94
94
|
reviewUrl: new URL(output.reviewUrl ?? `/drafts/${output.update.id}/edit`, `${client.apiUrl}/`).toString(),
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
-
async function resolveProject(client, ref) {
|
|
98
|
-
// A UUID already carries the exact project boundary. Do not require a
|
|
99
|
-
// global /projects listing first: project-restricted keys deliberately
|
|
100
|
-
// may not have projects:read across their human's whole account.
|
|
101
|
-
if (z.uuid().safeParse(ref).success) {
|
|
102
|
-
return { id: ref, slug: ref, name: ref };
|
|
103
|
-
}
|
|
104
|
-
// Accept a project id, global handle, or legacy owner-scoped slug; try all
|
|
105
|
-
// so a hex-and-dash slug that merely looks uuid-shaped still resolves.
|
|
106
|
-
const data = await client.request("/projects");
|
|
107
|
-
const project = data.projects.find((row) => row.id === ref) ??
|
|
108
|
-
data.projects.find((row) => row.handle === ref.toLowerCase()) ??
|
|
109
|
-
data.projects.find((row) => row.slug === ref.toLowerCase());
|
|
110
|
-
if (!project) {
|
|
111
|
-
const known = data.projects.map((row) => row.handle ?? row.slug).join(", ") || "(none)";
|
|
112
|
-
throw new Error(`No project "${ref}". Known projects: ${known}`);
|
|
113
|
-
}
|
|
114
|
-
return project;
|
|
115
|
-
}
|
|
116
97
|
function canUse(scopes, permission, projectAware = false) {
|
|
117
98
|
return projectAware
|
|
118
99
|
? scopesAllowAnyProject(scopes, permission)
|
|
@@ -139,16 +120,14 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
139
120
|
if (canUse(scopes, "projects:read", true)) {
|
|
140
121
|
server.registerTool("get_project", {
|
|
141
122
|
title: "Get one Cindrel project",
|
|
142
|
-
description: "Fetch one project owned by the human this agent works with, including its declared tags and canonical URL. Requires projects:read permission for the selected project
|
|
123
|
+
description: "Fetch one project owned by the human this agent works with, including its declared tags and canonical URL. Requires projects:read permission for the selected project; project-restricted handles resolve only among granted projects.",
|
|
143
124
|
inputSchema: {
|
|
144
|
-
project: z
|
|
145
|
-
.string()
|
|
146
|
-
.describe("Project handle, legacy slug, or id (see list_projects)"),
|
|
125
|
+
project: z.string().describe(PROJECT_REF_DESCRIPTION),
|
|
147
126
|
},
|
|
148
127
|
outputSchema: ProjectOutputSchema,
|
|
149
128
|
annotations: READ_ANNOTATIONS,
|
|
150
129
|
}, async ({ project }) => {
|
|
151
|
-
const resolved = await resolveProject(client, project);
|
|
130
|
+
const resolved = await resolveProject(client, scopes, project);
|
|
152
131
|
return structuredResult(ProjectOutputSchema.parse(await client.request(`/projects/${encodeURIComponent(resolved.id)}`)));
|
|
153
132
|
});
|
|
154
133
|
}
|
|
@@ -214,11 +193,9 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
214
193
|
if (canUse(scopes, "projects:write", true)) {
|
|
215
194
|
server.registerTool("update_project", {
|
|
216
195
|
title: "Update a Cindrel project",
|
|
217
|
-
description: "Update an existing project. Requires projects:write permission for the selected project.
|
|
196
|
+
description: "Update an existing project. Requires projects:write permission for the selected project. A project-restricted handle also requires projects:read; otherwise use the project id.",
|
|
218
197
|
inputSchema: {
|
|
219
|
-
project: z
|
|
220
|
-
.string()
|
|
221
|
-
.describe("Project handle, legacy slug, or id (see list_projects)"),
|
|
198
|
+
project: z.string().describe(PROJECT_REF_DESCRIPTION),
|
|
222
199
|
handle: PROJECT_HANDLE.optional().describe("New globally unique public handle; the old handle remains a redirect"),
|
|
223
200
|
name: z.string().min(1).max(80).optional(),
|
|
224
201
|
tagline: z.string().max(140).nullable().optional(),
|
|
@@ -238,7 +215,7 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
238
215
|
outputSchema: ProjectOutputSchema,
|
|
239
216
|
annotations: MODIFY_ANNOTATIONS,
|
|
240
217
|
}, async ({ project, ...changes }) => {
|
|
241
|
-
const resolved = await resolveProject(client, project);
|
|
218
|
+
const resolved = await resolveProject(client, scopes, project);
|
|
242
219
|
return structuredResult(ProjectOutputSchema.parse(await client.request(`/projects/${encodeURIComponent(resolved.id)}`, {
|
|
243
220
|
method: "PATCH",
|
|
244
221
|
body: changes,
|
|
@@ -249,12 +226,10 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
249
226
|
canUse(scopes, "projects:read", true)) {
|
|
250
227
|
server.registerTool("list_project_profile_proposals", {
|
|
251
228
|
title: "List pending project profile proposals",
|
|
252
|
-
description: "List private profile changes awaiting the human owner's review for one project.
|
|
229
|
+
description: "List private profile changes awaiting the human owner's review for one project. Project-restricted handles resolve only among granted projects." +
|
|
253
230
|
UNTRUSTED_CONTENT_NOTE,
|
|
254
231
|
inputSchema: {
|
|
255
|
-
project: z
|
|
256
|
-
.string()
|
|
257
|
-
.describe("Project handle, legacy slug, or id (see list_projects)"),
|
|
232
|
+
project: z.string().describe(PROJECT_REF_DESCRIPTION),
|
|
258
233
|
before: z
|
|
259
234
|
.uuid()
|
|
260
235
|
.optional()
|
|
@@ -270,7 +245,7 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
270
245
|
outputSchema: ProjectProfileProposalsOutputSchema,
|
|
271
246
|
annotations: READ_ANNOTATIONS,
|
|
272
247
|
}, async ({ project, before, limit }) => {
|
|
273
|
-
const resolved = await resolveProject(client, project);
|
|
248
|
+
const resolved = await resolveProject(client, scopes, project);
|
|
274
249
|
const query = new URLSearchParams({ limit: String(limit) });
|
|
275
250
|
if (before)
|
|
276
251
|
query.set("before", before);
|
|
@@ -287,7 +262,7 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
287
262
|
outputSchema: ProjectProfileProposalOutputSchema,
|
|
288
263
|
annotations: CREATE_ANNOTATIONS,
|
|
289
264
|
}, async ({ project, ...changes }) => {
|
|
290
|
-
const resolved = await resolveProject(client, project);
|
|
265
|
+
const resolved = await resolveProject(client, scopes, project);
|
|
291
266
|
return structuredResult(ProjectProfileProposalOutputSchema.parse(await client.request(`/projects/${encodeURIComponent(resolved.id)}/profile-proposals`, { method: "POST", body: changes })));
|
|
292
267
|
});
|
|
293
268
|
}
|
|
@@ -299,9 +274,7 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
299
274
|
? "Create a build-log update. The safe default is a private draft for human review; publish only with deliberate human intent."
|
|
300
275
|
: "Create a private build-log draft for human review. This key cannot publish directly.",
|
|
301
276
|
inputSchema: {
|
|
302
|
-
project: z
|
|
303
|
-
.string()
|
|
304
|
-
.describe("Project handle, legacy slug, or id (see list_projects)"),
|
|
277
|
+
project: z.string().describe(PROJECT_REF_DESCRIPTION),
|
|
305
278
|
title: z.string().max(140).optional().describe("Optional headline"),
|
|
306
279
|
body: z
|
|
307
280
|
.string()
|
|
@@ -330,7 +303,7 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
330
303
|
outputSchema: CreatedUpdateOutputSchema,
|
|
331
304
|
annotations: CREATE_ANNOTATIONS,
|
|
332
305
|
}, async ({ project, ...update }) => {
|
|
333
|
-
const resolved = await resolveProject(client, project);
|
|
306
|
+
const resolved = await resolveProject(client, scopes, project);
|
|
334
307
|
return structuredResult(normalizeCreatedUpdateOutput(client, await client.request(`/projects/${encodeURIComponent(resolved.id)}/updates`, { method: "POST", body: update })));
|
|
335
308
|
});
|
|
336
309
|
}
|
|
@@ -340,9 +313,7 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
340
313
|
description: "List updates, including private drafts, on one of the human's projects. Pass nextCursor back as before to continue. Draft bodies can embed text from external sources (e.g. GitHub commit messages)." +
|
|
341
314
|
UNTRUSTED_CONTENT_NOTE,
|
|
342
315
|
inputSchema: {
|
|
343
|
-
project: z
|
|
344
|
-
.string()
|
|
345
|
-
.describe("Project handle, legacy slug, or id (see list_projects)"),
|
|
316
|
+
project: z.string().describe(PROJECT_REF_DESCRIPTION),
|
|
346
317
|
before: z
|
|
347
318
|
.uuid()
|
|
348
319
|
.optional()
|
|
@@ -358,7 +329,7 @@ export function buildServer(client, scopes, features = {}) {
|
|
|
358
329
|
outputSchema: UpdatesOutputSchema,
|
|
359
330
|
annotations: READ_ANNOTATIONS,
|
|
360
331
|
}, async ({ project, before, limit }) => {
|
|
361
|
-
const resolved = await resolveProject(client, project);
|
|
332
|
+
const resolved = await resolveProject(client, scopes, project);
|
|
362
333
|
const search = new URLSearchParams({ limit: String(limit) });
|
|
363
334
|
if (before)
|
|
364
335
|
search.set("before", before);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CindrelApiError } from "./client.js";
|
|
3
|
+
import { scopesAllow } from "./scopes.js";
|
|
4
|
+
function scopedReadableProjectIds(scopes) {
|
|
5
|
+
const suffix = ":projects:read";
|
|
6
|
+
const ids = new Set();
|
|
7
|
+
for (const scope of scopes) {
|
|
8
|
+
if (!scope.startsWith("project:") || !scope.endsWith(suffix))
|
|
9
|
+
continue;
|
|
10
|
+
const id = scope.slice("project:".length, -suffix.length);
|
|
11
|
+
if (z.uuid().safeParse(id).success)
|
|
12
|
+
ids.add(id);
|
|
13
|
+
}
|
|
14
|
+
return [...ids];
|
|
15
|
+
}
|
|
16
|
+
function projectMatches(project, ref) {
|
|
17
|
+
const normalized = ref.toLowerCase();
|
|
18
|
+
return (project.id === ref ||
|
|
19
|
+
project.handle?.toLowerCase() === normalized ||
|
|
20
|
+
project.slug.toLowerCase() === normalized);
|
|
21
|
+
}
|
|
22
|
+
function missingProject(ref, projects) {
|
|
23
|
+
const known = projects.map((project) => project.handle ?? project.slug).join(", ") ||
|
|
24
|
+
"(none)";
|
|
25
|
+
return new Error(`No project "${ref}". Known projects: ${known}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve only within projects the key may read. A project-restricted key can
|
|
29
|
+
* therefore use the same handle-oriented tools as a broad key without gaining
|
|
30
|
+
* access to the human's account-wide project list.
|
|
31
|
+
*/
|
|
32
|
+
export async function resolveProject(client, scopes, ref) {
|
|
33
|
+
// A UUID already carries the exact project boundary. The target endpoint
|
|
34
|
+
// authorizes it, so no discovery read is needed before the actual tool call.
|
|
35
|
+
if (z.uuid().safeParse(ref).success) {
|
|
36
|
+
return { id: ref, slug: ref, name: ref };
|
|
37
|
+
}
|
|
38
|
+
if (scopesAllow(scopes, "projects:read")) {
|
|
39
|
+
const data = await client.request("/projects");
|
|
40
|
+
const project = data.projects.find((row) => projectMatches(row, ref));
|
|
41
|
+
if (!project)
|
|
42
|
+
throw missingProject(ref, data.projects);
|
|
43
|
+
return project;
|
|
44
|
+
}
|
|
45
|
+
const readable = [];
|
|
46
|
+
for (const id of scopedReadableProjectIds(scopes)) {
|
|
47
|
+
try {
|
|
48
|
+
const data = await client.request(`/projects/${encodeURIComponent(id)}`);
|
|
49
|
+
readable.push(data.project);
|
|
50
|
+
if (projectMatches(data.project, ref))
|
|
51
|
+
return data.project;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// A deleted or newly unavailable project is simply no longer a
|
|
55
|
+
// candidate. Authentication, throttling, and server/network failures
|
|
56
|
+
// are actionable and must not be disguised as a missing handle.
|
|
57
|
+
if (error instanceof CindrelApiError && error.status === 404)
|
|
58
|
+
continue;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw missingProject(ref, readable);
|
|
63
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_VERSION = "0.9.
|
|
1
|
+
export const MCP_VERSION = "0.9.1";
|