@typelets/mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +58 -0
- package/dist/client.js.map +1 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +36 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/profile.d.ts +50 -0
- package/dist/profile.js +35 -0
- package/dist/profile.js.map +1 -0
- package/dist/tools/_shared.d.ts +17 -0
- package/dist/tools/_shared.js +55 -0
- package/dist/tools/_shared.js.map +1 -0
- package/dist/tools/apply_problem_to_workspace.d.ts +12 -0
- package/dist/tools/apply_problem_to_workspace.js +31 -0
- package/dist/tools/apply_problem_to_workspace.js.map +1 -0
- package/dist/tools/create_file.d.ts +11 -0
- package/dist/tools/create_file.js +29 -0
- package/dist/tools/create_file.js.map +1 -0
- package/dist/tools/create_workspace.d.ts +11 -0
- package/dist/tools/create_workspace.js +33 -0
- package/dist/tools/create_workspace.js.map +1 -0
- package/dist/tools/delete_file.d.ts +12 -0
- package/dist/tools/delete_file.js +26 -0
- package/dist/tools/delete_file.js.map +1 -0
- package/dist/tools/delete_problem.d.ts +11 -0
- package/dist/tools/delete_problem.js +29 -0
- package/dist/tools/delete_problem.js.map +1 -0
- package/dist/tools/edit_problem.d.ts +11 -0
- package/dist/tools/edit_problem.js +87 -0
- package/dist/tools/edit_problem.js.map +1 -0
- package/dist/tools/get_problem.d.ts +13 -0
- package/dist/tools/get_problem.js +27 -0
- package/dist/tools/get_problem.js.map +1 -0
- package/dist/tools/get_workspace.d.ts +12 -0
- package/dist/tools/get_workspace.js +23 -0
- package/dist/tools/get_workspace.js.map +1 -0
- package/dist/tools/list_pending_invites.d.ts +12 -0
- package/dist/tools/list_pending_invites.js +39 -0
- package/dist/tools/list_pending_invites.js.map +1 -0
- package/dist/tools/list_problems.d.ts +15 -0
- package/dist/tools/list_problems.js +38 -0
- package/dist/tools/list_problems.js.map +1 -0
- package/dist/tools/list_recordings.d.ts +12 -0
- package/dist/tools/list_recordings.js +20 -0
- package/dist/tools/list_recordings.js.map +1 -0
- package/dist/tools/list_workspace_files.d.ts +15 -0
- package/dist/tools/list_workspace_files.js +25 -0
- package/dist/tools/list_workspace_files.js.map +1 -0
- package/dist/tools/list_workspaces.d.ts +11 -0
- package/dist/tools/list_workspaces.js +20 -0
- package/dist/tools/list_workspaces.js.map +1 -0
- package/dist/tools/read_workspace_file.d.ts +16 -0
- package/dist/tools/read_workspace_file.js +24 -0
- package/dist/tools/read_workspace_file.js.map +1 -0
- package/dist/tools/save_problem_to_library.d.ts +11 -0
- package/dist/tools/save_problem_to_library.js +83 -0
- package/dist/tools/save_problem_to_library.js.map +1 -0
- package/dist/tools/update_file.d.ts +12 -0
- package/dist/tools/update_file.js +27 -0
- package/dist/tools/update_file.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Typelets
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# typelets-mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io/) server for [Typelets](https://typelets.com), the collaborative IDE for technical interviews and pair programming. Connect an MCP-capable client (Claude Desktop, Cline, Cursor, Codex, etc.) and let it list workspaces, browse the problem library, read candidate files, summarize sessions, and (later) write back to the workspace on your behalf.
|
|
4
|
+
|
|
5
|
+
Status: pre-alpha. The spec below is the plan; the server is being built in public.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Interviewers spend a lot of meta-work outside the coding window: authoring problems, prepping starter files, reviewing candidate code after the fact, summarizing what happened. Most of that work is "look at structured data, produce structured data," which is exactly what an LLM is good at. Wrapping the Typelets API as MCP tools lets the interviewer's AI of choice do that work without the platform itself needing to embed model logic.
|
|
10
|
+
|
|
11
|
+
## Phased plan
|
|
12
|
+
|
|
13
|
+
| Phase | Scope | Risk |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| 1 | Read-only: list workspaces, list problems, get problem detail, read workspace tree + file contents, fetch recording metadata, list pending invites | Low. Nothing the LLM does here can change platform state. |
|
|
16
|
+
| 2 | Authoring: create workspace, apply problem to workspace, draft/edit a library problem, materialize starter files | Medium. Writes happen, but always against the caller's own resources. |
|
|
17
|
+
| 3 | Session intelligence: summarize a recording transcript, draft scores against the rubric, suggest follow-up questions, batch-import problems from a corpus | Higher. Outputs land on candidate-facing records, so there is a review gate before they take effect. |
|
|
18
|
+
|
|
19
|
+
Phase 1 is the entire v0.1.0 release. Phase 2 lands in v0.2.x. Phase 3 is v0.3.x once we have a UX answer for "review before commit".
|
|
20
|
+
|
|
21
|
+
## How auth works
|
|
22
|
+
|
|
23
|
+
The server authenticates against the Typelets API using a **Personal Access Token**. Tokens are issued in User Settings -> Tokens on https://typelets.com, are scoped per-user, and carry an expiry plus a label. Tokens never leave the user's machine. The MCP server reads `TYPELETS_TOKEN` from its environment, and the client passes it through.
|
|
24
|
+
|
|
25
|
+
Why not OAuth: this is a single-user CLI tool wired into a desktop LLM client. PATs match the audience without dragging in a browser dance every session. An OAuth flow becomes plausible later if a Typelets-hosted MCP gateway makes sense.
|
|
26
|
+
|
|
27
|
+
Why not session cookies: cookies are scoped to a browser and expire on the platform's schedule, not the user's. A PAT is the right primitive for "a programmable agent acting as me."
|
|
28
|
+
|
|
29
|
+
The PAT issuance flow is the only prerequisite work on the Typelets side. Everything else in this repo just wraps existing endpoints.
|
|
30
|
+
|
|
31
|
+
## Two profiles
|
|
32
|
+
|
|
33
|
+
The server ships two profiles:
|
|
34
|
+
|
|
35
|
+
- **`interviewer`** (default): has access to rubric content, hidden tests, scores, and the full library.
|
|
36
|
+
- **`candidate`** (strict): hides rubric / hidden tests / scores even if the user's role would otherwise allow it. Useful when the user wants to point an AI assistant at their own in-progress interview without leaking the answer key into the LLM's context.
|
|
37
|
+
|
|
38
|
+
The profile is set at start-up, not per-tool:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
TYPELETS_PROFILE=candidate npx @typelets/mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Default is `interviewer`. The server refuses to switch profiles at runtime. If you want both, run two server instances on different ports.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx @typelets/mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
In your MCP client's config (Claude Desktop, Cline, Cursor, etc.):
|
|
53
|
+
|
|
54
|
+
```jsonc
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"typelets": {
|
|
58
|
+
"command": "npx",
|
|
59
|
+
"args": ["@typelets/mcp"],
|
|
60
|
+
"env": {
|
|
61
|
+
"TYPELETS_TOKEN": "pat_…",
|
|
62
|
+
"TYPELETS_API_URL": "https://api.typelets.com",
|
|
63
|
+
"TYPELETS_PROFILE": "interviewer"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Get your token at https://typelets.com: User Settings → Tokens → New token. Give it a label like "Claude Desktop on laptop", pick an expiry, copy the value (you only see it once), and paste it into `TYPELETS_TOKEN`.
|
|
71
|
+
|
|
72
|
+
Profiles:
|
|
73
|
+
- `interviewer` (default): full surface, including rubric and hidden tests.
|
|
74
|
+
- `candidate`: the same tools but rubric, criteria, hidden tests, and solution are stripped before they reach the LLM. Use this when you want an AI assistant helping you on a problem you're solving.
|
|
75
|
+
|
|
76
|
+
## Tool surface
|
|
77
|
+
|
|
78
|
+
16 tools total. In `candidate` profile, the 5 interviewer-only tools are not registered; the candidate's host LLM does not see them in `listTools`.
|
|
79
|
+
|
|
80
|
+
### Reads (8 tools, both profiles)
|
|
81
|
+
|
|
82
|
+
| Tool | Reads | Notes |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `list_workspaces` | `GET /workspaces` | Caller's workspaces, with role + mode. |
|
|
85
|
+
| `get_workspace` | `GET /workspaces/:id` | Full summary, share scope, applied problem id. |
|
|
86
|
+
| `list_workspace_files` | `GET /workspaces/:id/files` | Flat list of file ids + slash-separated paths. |
|
|
87
|
+
| `read_workspace_file` | `GET /workspaces/:id/files/:fileId/content` | UTF-8 content at HEAD; 1 MiB cap with `truncated` flag. |
|
|
88
|
+
| `list_problems` | `GET /problems` | Library entries the caller can see. |
|
|
89
|
+
| `get_problem` | `GET /problems/:id` | Prompt + criteria. Rubric / hidden tests stripped in candidate profile. |
|
|
90
|
+
| `list_recordings` | `GET /workspaces/:id/recordings` | Metadata only. |
|
|
91
|
+
| `list_pending_invites` | `GET /invites` + `GET /invitations` | Merged workspace + org invites. |
|
|
92
|
+
|
|
93
|
+
### Writes: file CRUD (3 tools, both profiles)
|
|
94
|
+
|
|
95
|
+
| Tool | Writes | `destructive` |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `create_file` | `POST /workspaces/:id/files` | |
|
|
98
|
+
| `update_file` | `PUT /workspaces/:id/files/:fileId/content` | ✓ |
|
|
99
|
+
| `delete_file` | `DELETE /workspaces/:id/files/:fileId` | ✓ |
|
|
100
|
+
|
|
101
|
+
### Writes: authoring (5 tools, interviewer profile only)
|
|
102
|
+
|
|
103
|
+
| Tool | Writes | `destructive` |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `create_workspace` | `POST /workspaces` | |
|
|
106
|
+
| `apply_problem_to_workspace` | `POST /workspaces/:id/interview/problem` | ✓ |
|
|
107
|
+
| `save_problem_to_library` | `POST /problems` | |
|
|
108
|
+
| `edit_problem` | `PATCH /problems/:id` | ✓ |
|
|
109
|
+
| `delete_problem` | `DELETE /problems/:id` | ✓ |
|
|
110
|
+
|
|
111
|
+
Tools marked `destructive` carry the MCP `destructiveHint: true` annotation so host clients (Claude Desktop, Cline, Cursor) prompt the user before invocation.
|
|
112
|
+
|
|
113
|
+
## Layout
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
typelets-mcp/
|
|
117
|
+
├─ README.md (this file)
|
|
118
|
+
├─ LICENSE
|
|
119
|
+
├─ package.json
|
|
120
|
+
├─ tsconfig.json
|
|
121
|
+
├─ .gitignore
|
|
122
|
+
├─ .github/
|
|
123
|
+
│ └─ workflows/
|
|
124
|
+
│ └─ ci.yml (typecheck + lint + test on push)
|
|
125
|
+
├─ src/
|
|
126
|
+
│ ├─ index.ts (server entry; wires stdio transport + handlers)
|
|
127
|
+
│ ├─ env.ts (parses TYPELETS_TOKEN / API_URL / PROFILE)
|
|
128
|
+
│ ├─ client.ts (thin fetch wrapper around the Typelets API)
|
|
129
|
+
│ ├─ profile.ts (interviewer vs candidate gating)
|
|
130
|
+
│ └─ tools/
|
|
131
|
+
│ ├─ list_workspaces.ts
|
|
132
|
+
│ ├─ get_workspace.ts
|
|
133
|
+
│ ├─ list_workspace_files.ts
|
|
134
|
+
│ ├─ read_workspace_file.ts
|
|
135
|
+
│ ├─ list_problems.ts
|
|
136
|
+
│ ├─ get_problem.ts
|
|
137
|
+
│ ├─ list_recordings.ts
|
|
138
|
+
│ └─ list_pending_invites.ts
|
|
139
|
+
└─ tests/ (unit tests + recorded API fixtures)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Contributing
|
|
143
|
+
|
|
144
|
+
Once v0.1.0 ships and the PAT flow is live on https://typelets.com, the repo will be open to issues + PRs. For now, watch the milestones tab.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT. See [LICENSE](./LICENSE).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin fetch wrapper around the Typelets API. Adds the Authorization
|
|
3
|
+
* header, JSON content-type, and a uniform error shape so individual
|
|
4
|
+
* tool handlers don't have to repeat the same plumbing.
|
|
5
|
+
*
|
|
6
|
+
* The server intentionally does NOT cache responses. Each tool call is
|
|
7
|
+
* a fresh request so the LLM never sees stale data, and the user keeps
|
|
8
|
+
* a real-time view of platform state from inside the chat.
|
|
9
|
+
*/
|
|
10
|
+
import type { Env } from './env.js';
|
|
11
|
+
export declare class TypeletsApiError extends Error {
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly body?: unknown | undefined;
|
|
14
|
+
constructor(status: number, message: string, body?: unknown | undefined);
|
|
15
|
+
}
|
|
16
|
+
export interface TypeletsClient {
|
|
17
|
+
get<T>(path: string): Promise<T>;
|
|
18
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
19
|
+
put<T>(path: string, body?: unknown): Promise<T>;
|
|
20
|
+
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
21
|
+
delete<T>(path: string): Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createClient(env: Env): TypeletsClient;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export class TypeletsApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
body;
|
|
4
|
+
constructor(status, message, body) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.body = body;
|
|
8
|
+
this.name = 'TypeletsApiError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function createClient(env) {
|
|
12
|
+
async function request(method, path, body) {
|
|
13
|
+
const url = `${env.apiUrl}${path}`;
|
|
14
|
+
const headers = {
|
|
15
|
+
accept: 'application/json',
|
|
16
|
+
authorization: `Bearer ${env.token}`,
|
|
17
|
+
};
|
|
18
|
+
if (body !== undefined)
|
|
19
|
+
headers['content-type'] = 'application/json';
|
|
20
|
+
// exactOptionalPropertyTypes refuses an explicit `undefined`; build
|
|
21
|
+
// the init object incrementally so `body` is only present when we
|
|
22
|
+
// actually have one.
|
|
23
|
+
const init = { method, headers };
|
|
24
|
+
if (body !== undefined)
|
|
25
|
+
init.body = JSON.stringify(body);
|
|
26
|
+
const res = await fetch(url, init);
|
|
27
|
+
if (res.status === 204)
|
|
28
|
+
return undefined;
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
let json = null;
|
|
31
|
+
if (text.length > 0) {
|
|
32
|
+
try {
|
|
33
|
+
json = JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-JSON response body (HTML error page, plain text). Leave
|
|
37
|
+
// json as null so the caller can decide whether to keep going
|
|
38
|
+
// or fail; the raw text rides along on the error for debugging.
|
|
39
|
+
json = text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const message = (json !== null && typeof json === 'object' && 'error' in json && typeof json.error === 'string')
|
|
44
|
+
? json.error
|
|
45
|
+
: `Typelets API ${method} ${path} failed with ${res.status}`;
|
|
46
|
+
throw new TypeletsApiError(res.status, message, json);
|
|
47
|
+
}
|
|
48
|
+
return json;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
get: (path) => request('GET', path),
|
|
52
|
+
post: (path, body) => request('POST', path, body),
|
|
53
|
+
put: (path, body) => request('PUT', path, body),
|
|
54
|
+
patch: (path, body) => request('PATCH', path, body),
|
|
55
|
+
delete: (path) => request('DELETE', path),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAEvB;IAEA;IAHlB,YACkB,MAAc,EAC9B,OAAe,EACC,IAAc;QAE9B,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,WAAM,GAAN,MAAM,CAAQ;QAEd,SAAI,GAAJ,IAAI,CAAU;QAG9B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAUD,MAAM,UAAU,YAAY,CAAC,GAAQ;IACnC,KAAK,UAAU,OAAO,CACpB,MAAmD,EACnD,IAAY,EACZ,IAAc;QAEd,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QACnC,MAAM,OAAO,GAA2B;YACtC,MAAM,EAAE,kBAAkB;YAC1B,aAAa,EAAE,UAAU,GAAG,CAAC,KAAK,EAAE;SACrC,CAAC;QACF,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAC;QAErE,oEAAoE;QACpE,kEAAkE;QAClE,qBAAqB;QACrB,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QAC9C,IAAI,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACzD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAEnC,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,SAAc,CAAC;QAE9C,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,IAAI,GAAY,IAAI,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpB,IAAI,CAAC;gBACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC;YAAC,MAAM,CAAC;gBACP,8DAA8D;gBAC9D,8DAA8D;gBAC9D,gEAAgE;gBAChE,IAAI,GAAG,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,OAAO,GACX,CAAC,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC;gBAC9F,CAAC,CAAC,IAAI,CAAC,KAAK;gBACZ,CAAC,CAAC,gBAAgB,MAAM,IAAI,IAAI,gBAAgB,GAAG,CAAC,MAAM,EAAE,CAAC;YACjE,MAAM,IAAI,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QACxD,CAAC;QAED,OAAO,IAAS,CAAC;IACnB,CAAC;IAED,OAAO;QACL,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC;QACnC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC;QACjD,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC;QAC/C,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;QACnD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;KAC1C,CAAC;AACJ,CAAC"}
|
package/dist/env.d.ts
ADDED
package/dist/env.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration the server reads from its process environment at start-up.
|
|
3
|
+
* The MCP server is invoked by a client (Claude Desktop, Cline, etc.) which
|
|
4
|
+
* supplies these via the client's mcpServers config, not by the user
|
|
5
|
+
* directly, so a missing or malformed value should surface as a clear
|
|
6
|
+
* stderr message before any tool registration happens.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
/**
|
|
10
|
+
* Personal Access Token issued at typelets.com/settings/tokens. Required.
|
|
11
|
+
* Stored on disk only in the host client's MCP config; never leaves the
|
|
12
|
+
* caller's machine.
|
|
13
|
+
*/
|
|
14
|
+
const tokenSchema = z.string().min(20, 'TYPELETS_TOKEN looks too short to be a real PAT.');
|
|
15
|
+
/**
|
|
16
|
+
* API base URL. Defaults to production. Local development against a
|
|
17
|
+
* self-hosted Typelets instance overrides this.
|
|
18
|
+
*/
|
|
19
|
+
const apiUrlSchema = z
|
|
20
|
+
.string()
|
|
21
|
+
.url()
|
|
22
|
+
.default('https://api.typelets.com');
|
|
23
|
+
/**
|
|
24
|
+
* Which tool surface to expose. `interviewer` (default) sees rubric +
|
|
25
|
+
* hidden test content. `candidate` strips it out so an AI assistant
|
|
26
|
+
* helping a candidate can not be fed the answer key. The profile is
|
|
27
|
+
* fixed at start-up; the server refuses to switch it at runtime.
|
|
28
|
+
*/
|
|
29
|
+
const profileSchema = z.enum(['interviewer', 'candidate']).default('interviewer');
|
|
30
|
+
export function readEnv() {
|
|
31
|
+
const token = tokenSchema.parse(process.env.TYPELETS_TOKEN);
|
|
32
|
+
const apiUrl = apiUrlSchema.parse(process.env.TYPELETS_API_URL);
|
|
33
|
+
const profile = profileSchema.parse(process.env.TYPELETS_PROFILE);
|
|
34
|
+
return { token, apiUrl, profile };
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=env.js.map
|
package/dist/env.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,kDAAkD,CAAC,CAAC;AAE3F;;;GAGG;AACH,MAAM,YAAY,GAAG,CAAC;KACnB,MAAM,EAAE;KACR,GAAG,EAAE;KACL,OAAO,CAAC,0BAA0B,CAAC,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;AAQlF,MAAM,UAAU,OAAO;IACrB,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACpC,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Entry point for the Typelets MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Runs over stdio (the transport every desktop MCP client speaks today).
|
|
6
|
+
* On start-up we:
|
|
7
|
+
* 1. Read + validate the env (token, api url, profile).
|
|
8
|
+
* 2. Build a thin API client.
|
|
9
|
+
* 3. Register the Phase 1 read tools and the Phase 2 write tools.
|
|
10
|
+
* 4. Hand off to the MCP SDK's stdio transport loop.
|
|
11
|
+
*
|
|
12
|
+
* Phase 2 write tools are profile-gated at registration time.
|
|
13
|
+
* interviewer-only tools are not registered when TYPELETS_PROFILE=candidate.
|
|
14
|
+
* Phase 3 (session intelligence) lands in a later minor release.
|
|
15
|
+
*/
|
|
16
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
+
import { readEnv } from './env.js';
|
|
19
|
+
import { createClient } from './client.js';
|
|
20
|
+
import { registerListWorkspaces } from './tools/list_workspaces.js';
|
|
21
|
+
import { registerGetWorkspace } from './tools/get_workspace.js';
|
|
22
|
+
import { registerListWorkspaceFiles } from './tools/list_workspace_files.js';
|
|
23
|
+
import { registerReadWorkspaceFile } from './tools/read_workspace_file.js';
|
|
24
|
+
import { registerListProblems } from './tools/list_problems.js';
|
|
25
|
+
import { registerGetProblem } from './tools/get_problem.js';
|
|
26
|
+
import { registerListRecordings } from './tools/list_recordings.js';
|
|
27
|
+
import { registerListPendingInvites } from './tools/list_pending_invites.js';
|
|
28
|
+
import { registerCreateFile } from './tools/create_file.js';
|
|
29
|
+
import { registerUpdateFile } from './tools/update_file.js';
|
|
30
|
+
import { registerDeleteFile } from './tools/delete_file.js';
|
|
31
|
+
import { registerCreateWorkspace } from './tools/create_workspace.js';
|
|
32
|
+
import { registerApplyProblemToWorkspace } from './tools/apply_problem_to_workspace.js';
|
|
33
|
+
import { registerSaveProblemToLibrary } from './tools/save_problem_to_library.js';
|
|
34
|
+
import { registerEditProblem } from './tools/edit_problem.js';
|
|
35
|
+
import { registerDeleteProblem } from './tools/delete_problem.js';
|
|
36
|
+
async function main() {
|
|
37
|
+
const env = readEnv();
|
|
38
|
+
const client = createClient(env);
|
|
39
|
+
const server = new McpServer({
|
|
40
|
+
name: 'typelets-mcp',
|
|
41
|
+
version: '0.0.1',
|
|
42
|
+
}, {
|
|
43
|
+
capabilities: {
|
|
44
|
+
tools: {},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
// Phase 1 read-only tools. Each module owns its own input schema,
|
|
48
|
+
// description, and handler so adding/removing tools is a single-file
|
|
49
|
+
// change and the entry stays a flat registration list.
|
|
50
|
+
registerListWorkspaces(server, client, env);
|
|
51
|
+
registerGetWorkspace(server, client, env);
|
|
52
|
+
registerListWorkspaceFiles(server, client, env);
|
|
53
|
+
registerReadWorkspaceFile(server, client, env);
|
|
54
|
+
registerListProblems(server, client, env);
|
|
55
|
+
registerGetProblem(server, client, env);
|
|
56
|
+
registerListRecordings(server, client, env);
|
|
57
|
+
registerListPendingInvites(server, client, env);
|
|
58
|
+
// Phase 2 write tools (profile-gated inside each register function).
|
|
59
|
+
registerCreateFile(server, client, env);
|
|
60
|
+
registerUpdateFile(server, client, env);
|
|
61
|
+
registerDeleteFile(server, client, env);
|
|
62
|
+
registerCreateWorkspace(server, client, env);
|
|
63
|
+
registerApplyProblemToWorkspace(server, client, env);
|
|
64
|
+
registerSaveProblemToLibrary(server, client, env);
|
|
65
|
+
registerEditProblem(server, client, env);
|
|
66
|
+
registerDeleteProblem(server, client, env);
|
|
67
|
+
const transport = new StdioServerTransport();
|
|
68
|
+
await server.connect(transport);
|
|
69
|
+
// Log to stderr (stdout is the MCP transport). Surfaces in the client's
|
|
70
|
+
// server log so the user can confirm the profile they're running.
|
|
71
|
+
process.stderr.write(`typelets-mcp running. profile=${env.profile} api=${env.apiUrl}\n`);
|
|
72
|
+
}
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
process.stderr.write(`typelets-mcp failed to start: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-aware filtering for outbound tool responses.
|
|
3
|
+
*
|
|
4
|
+
* The `candidate` profile strips fields the candidate must not see even
|
|
5
|
+
* if their auth token would let them: hidden tests, rubric content,
|
|
6
|
+
* scores, solution files. The `interviewer` profile is a pass-through.
|
|
7
|
+
*
|
|
8
|
+
* The point is defense in depth. The API already enforces these
|
|
9
|
+
* boundaries on a per-role basis, but the candidate profile is a
|
|
10
|
+
* second gate the user opts into when they don't want their AI
|
|
11
|
+
* assistant ingesting answer-key content along with the prompt.
|
|
12
|
+
*/
|
|
13
|
+
import type { Env } from './env.js';
|
|
14
|
+
export interface ProblemDetailLike {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
prompt: string;
|
|
18
|
+
difficulty: string;
|
|
19
|
+
category: string;
|
|
20
|
+
tags: string[];
|
|
21
|
+
/** Interviewer-only. Removed in candidate profile. */
|
|
22
|
+
rubric?: string | null;
|
|
23
|
+
/** Interviewer-only. Removed in candidate profile. */
|
|
24
|
+
criteria?: {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
}[];
|
|
28
|
+
/** Interviewer-only. Hidden tests stripped; visible tests remain. */
|
|
29
|
+
tests?: {
|
|
30
|
+
input: string;
|
|
31
|
+
expectedOutput: string;
|
|
32
|
+
visible: boolean;
|
|
33
|
+
}[];
|
|
34
|
+
starters?: Record<string, unknown>;
|
|
35
|
+
/** Interviewer-only. Removed entirely in candidate profile. */
|
|
36
|
+
solution?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Names of tools that the candidate profile MUST NOT register. The
|
|
40
|
+
* server skips registration for these when `env.profile === 'candidate'`,
|
|
41
|
+
* so the candidate's host LLM never sees them in `listTools` and cannot
|
|
42
|
+
* try to invoke them.
|
|
43
|
+
*
|
|
44
|
+
* Per-file CRUD (create_file, update_file, delete_file) is allowed for
|
|
45
|
+
* candidates: they have editor role inside their own interview
|
|
46
|
+
* workspace and need to be able to modify their files.
|
|
47
|
+
*/
|
|
48
|
+
export declare const INTERVIEWER_ONLY_TOOLS: ReadonlySet<string>;
|
|
49
|
+
export declare function toolAllowedForProfile(toolName: string, profile: Env['profile']): boolean;
|
|
50
|
+
export declare function filterProblemForProfile<T extends ProblemDetailLike>(problem: T, env: Env): T;
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names of tools that the candidate profile MUST NOT register. The
|
|
3
|
+
* server skips registration for these when `env.profile === 'candidate'`,
|
|
4
|
+
* so the candidate's host LLM never sees them in `listTools` and cannot
|
|
5
|
+
* try to invoke them.
|
|
6
|
+
*
|
|
7
|
+
* Per-file CRUD (create_file, update_file, delete_file) is allowed for
|
|
8
|
+
* candidates: they have editor role inside their own interview
|
|
9
|
+
* workspace and need to be able to modify their files.
|
|
10
|
+
*/
|
|
11
|
+
export const INTERVIEWER_ONLY_TOOLS = new Set([
|
|
12
|
+
'create_workspace',
|
|
13
|
+
'apply_problem_to_workspace',
|
|
14
|
+
'save_problem_to_library',
|
|
15
|
+
'edit_problem',
|
|
16
|
+
'delete_problem',
|
|
17
|
+
]);
|
|
18
|
+
export function toolAllowedForProfile(toolName, profile) {
|
|
19
|
+
if (profile === 'interviewer')
|
|
20
|
+
return true;
|
|
21
|
+
return !INTERVIEWER_ONLY_TOOLS.has(toolName);
|
|
22
|
+
}
|
|
23
|
+
export function filterProblemForProfile(problem, env) {
|
|
24
|
+
if (env.profile === 'interviewer')
|
|
25
|
+
return problem;
|
|
26
|
+
const next = { ...problem };
|
|
27
|
+
delete next.rubric;
|
|
28
|
+
delete next.criteria;
|
|
29
|
+
delete next.solution;
|
|
30
|
+
if (Array.isArray(next.tests)) {
|
|
31
|
+
next.tests = next.tests.filter((t) => t.visible === true);
|
|
32
|
+
}
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"AAgCA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAwB,IAAI,GAAG,CAAC;IACjE,kBAAkB;IAClB,4BAA4B;IAC5B,yBAAyB;IACzB,cAAc;IACd,gBAAgB;CACjB,CAAC,CAAC;AAEH,MAAM,UAAU,qBAAqB,CAAC,QAAgB,EAAE,OAAuB;IAC7E,IAAI,OAAO,KAAK,aAAa;QAAE,OAAO,IAAI,CAAC;IAC3C,OAAO,CAAC,sBAAsB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,OAAU,EACV,GAAQ;IAER,IAAI,GAAG,CAAC,OAAO,KAAK,aAAa;QAAE,OAAO,OAAO,CAAC;IAClD,MAAM,IAAI,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC;IAC5B,OAAO,IAAI,CAAC,MAAM,CAAC;IACnB,OAAO,IAAI,CAAC,QAAQ,CAAC;IACrB,OAAO,IAAI,CAAC,QAAQ,CAAC;IACrB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ToolContentItem {
|
|
2
|
+
type: 'text';
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolResult {
|
|
6
|
+
[x: string]: unknown;
|
|
7
|
+
content: ToolContentItem[];
|
|
8
|
+
isError?: boolean;
|
|
9
|
+
structuredContent?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/** Build a successful tool response. `summary` is human-readable; `data`
|
|
12
|
+
* is the structured payload the model should reason over. */
|
|
13
|
+
export declare function ok(summary: string, data: unknown): ToolResult;
|
|
14
|
+
/** Build an error tool response. Maps TypeletsApiError to a readable
|
|
15
|
+
* message and preserves the underlying status so the model can branch
|
|
16
|
+
* on it (e.g. 404 -> ask the user for a different id). */
|
|
17
|
+
export declare function fail(err: unknown): ToolResult;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared across tool handlers.
|
|
3
|
+
*
|
|
4
|
+
* The MCP tool response shape is `{ content: ContentItem[], isError?: boolean }`.
|
|
5
|
+
* Every Phase 1 tool returns the same structure:
|
|
6
|
+
* - a `text` block carrying a short human-readable summary
|
|
7
|
+
* - a `text` block carrying the JSON payload, fenced so the host LLM
|
|
8
|
+
* can pick it out cleanly
|
|
9
|
+
*
|
|
10
|
+
* Keeping the shape uniform means downstream LLMs can rely on the
|
|
11
|
+
* "first line is summary, fenced block is data" contract without
|
|
12
|
+
* tool-specific parsing.
|
|
13
|
+
*/
|
|
14
|
+
import { TypeletsApiError } from '../client.js';
|
|
15
|
+
/** Build a successful tool response. `summary` is human-readable; `data`
|
|
16
|
+
* is the structured payload the model should reason over. */
|
|
17
|
+
export function ok(summary, data) {
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{ type: 'text', text: summary },
|
|
21
|
+
{ type: 'text', text: `\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\`` },
|
|
22
|
+
],
|
|
23
|
+
structuredContent: { result: data },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Build an error tool response. Maps TypeletsApiError to a readable
|
|
27
|
+
* message and preserves the underlying status so the model can branch
|
|
28
|
+
* on it (e.g. 404 -> ask the user for a different id). */
|
|
29
|
+
export function fail(err) {
|
|
30
|
+
if (err instanceof TypeletsApiError) {
|
|
31
|
+
const bodyDetails = err.body !== null &&
|
|
32
|
+
typeof err.body === 'object' &&
|
|
33
|
+
'details' in err.body &&
|
|
34
|
+
Array.isArray(err.body.details)
|
|
35
|
+
? `\nValidation details: ${JSON.stringify(err.body.details)}`
|
|
36
|
+
: '';
|
|
37
|
+
return {
|
|
38
|
+
isError: true,
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: `Typelets API error ${err.status}: ${err.message}${bodyDetails}`,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
structuredContent: { error: { status: err.status, message: err.message, body: err.body } },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
49
|
+
return {
|
|
50
|
+
isError: true,
|
|
51
|
+
content: [{ type: 'text', text: `tool failed: ${message}` }],
|
|
52
|
+
structuredContent: { error: { message } },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=_shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_shared.js","sourceRoot":"","sources":["../../src/tools/_shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAgBhD;8DAC8D;AAC9D,MAAM,UAAU,EAAE,CAAC,OAAe,EAAE,IAAa;IAC/C,OAAO;QACL,OAAO,EAAE;YACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;YAC/B,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,UAAU,EAAE;SACjF;QACD,iBAAiB,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;KACpC,CAAC;AACJ,CAAC;AAED;;2DAE2D;AAC3D,MAAM,UAAU,IAAI,CAAC,GAAY;IAC/B,IAAI,GAAG,YAAY,gBAAgB,EAAE,CAAC;QACpC,MAAM,WAAW,GACf,GAAG,CAAC,IAAI,KAAK,IAAI;YACjB,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;YAC5B,SAAS,IAAI,GAAG,CAAC,IAAI;YACrB,KAAK,CAAC,OAAO,CAAE,GAAG,CAAC,IAAgC,CAAC,OAAO,CAAC;YAC1D,CAAC,CAAC,yBAAyB,IAAI,CAAC,SAAS,CAAE,GAAG,CAAC,IAAgC,CAAC,OAAO,CAAC,EAAE;YAC1F,CAAC,CAAC,EAAE,CAAC;QACT,OAAO;YACL,OAAO,EAAE,IAAI;YACb,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,sBAAsB,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,OAAO,GAAG,WAAW,EAAE;iBACvE;aACF;YACD,iBAAiB,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE;SAC3F,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,OAAO,EAAE,EAAE,CAAC;QAC5D,iBAAiB,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE;KAC1C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apply_problem_to_workspace: apply a library problem to an interview workspace.
|
|
3
|
+
*
|
|
4
|
+
* Interviewer-only. Materializes the problem's starter + test files into the
|
|
5
|
+
* workspace tree, sets the interview prompt/rubric/criteria/tests on the
|
|
6
|
+
* workspace, and REPLACES any previously applied files. This is destructive.
|
|
7
|
+
* a co-editor's in-progress work may be clobbered.
|
|
8
|
+
*/
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import type { TypeletsClient } from '../client.js';
|
|
11
|
+
import type { Env } from '../env.js';
|
|
12
|
+
export declare function registerApplyProblemToWorkspace(server: McpServer, client: TypeletsClient, env: Env): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { toolAllowedForProfile } from '../profile.js';
|
|
3
|
+
import { ok, fail } from './_shared.js';
|
|
4
|
+
export function registerApplyProblemToWorkspace(server, client, env) {
|
|
5
|
+
// Profile gate: keep the string in sync with INTERVIEWER_ONLY_TOOLS in ../profile.ts.
|
|
6
|
+
if (!toolAllowedForProfile('apply_problem_to_workspace', env.profile))
|
|
7
|
+
return;
|
|
8
|
+
server.registerTool('apply_problem_to_workspace', {
|
|
9
|
+
title: 'Apply a library problem to an interview workspace',
|
|
10
|
+
description: 'Apply a library problem to an interview workspace. Materializes the problem\'s starter files ' +
|
|
11
|
+
'and test files into the workspace tree and copies the prompt, rubric, criteria, and test cases ' +
|
|
12
|
+
'onto the workspace. Existing candidate work in the workspace is REPLACED: any previously ' +
|
|
13
|
+
'applied starter files are removed and the new ones are written in a single atomic pass. ' +
|
|
14
|
+
'Destructive. Confirm before invoking. Errors: 404 if the workspace or problem is not found; ' +
|
|
15
|
+
'403 if the caller is not owner/admin/interviewer of the workspace.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
workspaceId: z.string().min(1).describe('The workspace id from list_workspaces or create_workspace. Typically an interview-mode workspace. The API does not enforce this, but applying to a general workspace populates interview fields that the general-mode UI does not surface.'),
|
|
18
|
+
problemId: z.string().min(1).describe('The problem id or slug from list_problems. Accepts either the cuid or the slug.'),
|
|
19
|
+
},
|
|
20
|
+
annotations: { destructiveHint: true },
|
|
21
|
+
}, async ({ workspaceId, problemId }) => {
|
|
22
|
+
try {
|
|
23
|
+
const result = await client.post(`/workspaces/${encodeURIComponent(workspaceId)}/interview/problem`, { problemId });
|
|
24
|
+
return ok(`Applied problem ${problemId} to workspace ${workspaceId}.`, { workspace: result.workspace });
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return fail(err);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=apply_problem_to_workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply_problem_to_workspace.js","sourceRoot":"","sources":["../../src/tools/apply_problem_to_workspace.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAaxC,MAAM,UAAU,+BAA+B,CAAC,MAAiB,EAAE,MAAsB,EAAE,GAAQ;IACjG,sFAAsF;IACtF,IAAI,CAAC,qBAAqB,CAAC,4BAA4B,EAAE,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO;IAE9E,MAAM,CAAC,YAAY,CACjB,4BAA4B,EAC5B;QACE,KAAK,EAAE,mDAAmD;QAC1D,WAAW,EACT,+FAA+F;YAC/F,iGAAiG;YACjG,2FAA2F;YAC3F,0FAA0F;YAC1F,8FAA8F;YAC9F,oEAAoE;QACtE,WAAW,EAAE;YACX,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,4OAA4O,CAAC;YACrR,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,iFAAiF,CAAC;SACzH;QACD,WAAW,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE;KACvC,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,EAAE;QACnC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAC9B,eAAe,kBAAkB,CAAC,WAAW,CAAC,oBAAoB,EAClE,EAAE,SAAS,EAAE,CACd,CAAC;YACF,OAAO,EAAE,CACP,mBAAmB,SAAS,iBAAiB,WAAW,GAAG,EAC3D,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAChC,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create_file: create a new file at a path in a workspace.
|
|
3
|
+
*
|
|
4
|
+
* Available to both interviewer and candidate profiles. Candidates have
|
|
5
|
+
* editor role in their own interview workspace and need to create files.
|
|
6
|
+
* Intermediate folders are created automatically; returns the new file id.
|
|
7
|
+
*/
|
|
8
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import type { TypeletsClient } from '../client.js';
|
|
10
|
+
import type { Env } from '../env.js';
|
|
11
|
+
export declare function registerCreateFile(server: McpServer, client: TypeletsClient, env: Env): void;
|