mcp-pachca 0.1.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/src/setup.ts ADDED
@@ -0,0 +1,211 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import * as rl from "node:readline/promises";
5
+
6
+ const CLIENTS = ["Claude Code", "Cursor"] as const;
7
+ type Client = (typeof CLIENTS)[number];
8
+
9
+ const SCOPES = ["Project", "Global"] as const;
10
+ type Scope = (typeof SCOPES)[number];
11
+
12
+ function stderr(msg: string): void {
13
+ process.stderr.write(msg);
14
+ }
15
+
16
+ function stderrln(msg: string = ""): void {
17
+ process.stderr.write(msg + "\n");
18
+ }
19
+
20
+ // --- Prompt helpers ---
21
+
22
+ /** Collect all lines from a non-TTY stdin upfront (avoids readline buffering issues). */
23
+ function readAllLines(): Promise<string[]> {
24
+ return new Promise((resolve) => {
25
+ const lines: string[] = [];
26
+ const iface = rl.createInterface({ input: process.stdin });
27
+ iface.on("line", (line) => lines.push(line));
28
+ iface.on("close", () => resolve(lines));
29
+ });
30
+ }
31
+
32
+ /** Simple line-based prompter backed by a pre-read array (non-TTY). */
33
+ function makePipePrompter(lines: string[]) {
34
+ let idx = 0;
35
+ return {
36
+ ask(question: string): string {
37
+ stderr(question);
38
+ if (idx >= lines.length) {
39
+ stderrln("\nError: not enough input lines.");
40
+ process.exit(1);
41
+ }
42
+ const answer = lines[idx++]!.trim();
43
+ stderrln(answer);
44
+ return answer;
45
+ },
46
+ };
47
+ }
48
+
49
+ /** Interactive TTY prompter using readline/promises. */
50
+ function makeTTYPrompter() {
51
+ const iface = rl.createInterface({ input: process.stdin, output: process.stderr });
52
+ return {
53
+ async ask(question: string): Promise<string> {
54
+ const answer = await iface.question(question);
55
+ return answer.trim();
56
+ },
57
+ close() {
58
+ iface.close();
59
+ },
60
+ };
61
+ }
62
+
63
+ function askMaskedTTY(question: string): Promise<string> {
64
+ return new Promise((resolve) => {
65
+ stderr(question);
66
+ const buf: string[] = [];
67
+
68
+ process.stdin.setRawMode(true);
69
+ process.stdin.resume();
70
+ process.stdin.setEncoding("utf8");
71
+
72
+ const onData = (ch: string): void => {
73
+ const c = ch.toString();
74
+ switch (c) {
75
+ case "\n":
76
+ case "\r":
77
+ case "\u0004": // Ctrl-D
78
+ process.stdin.setRawMode(false);
79
+ process.stdin.pause();
80
+ process.stdin.removeListener("data", onData);
81
+ stderrln();
82
+ resolve(buf.join(""));
83
+ break;
84
+ case "\u0003": // Ctrl-C
85
+ process.stdin.setRawMode(false);
86
+ process.stdin.pause();
87
+ process.stdin.removeListener("data", onData);
88
+ stderrln("\nAborted.");
89
+ process.exit(1);
90
+ break;
91
+ case "\u007F": // Backspace
92
+ if (buf.length > 0) {
93
+ buf.pop();
94
+ stderr("\b \b");
95
+ }
96
+ break;
97
+ default:
98
+ buf.push(c);
99
+ stderr("*");
100
+ }
101
+ };
102
+
103
+ process.stdin.on("data", onData);
104
+ });
105
+ }
106
+
107
+ // --- Choice helper ---
108
+
109
+ async function askChoice<T extends string>(
110
+ ask: (q: string) => string | Promise<string>,
111
+ question: string,
112
+ choices: readonly T[],
113
+ ): Promise<T> {
114
+ stderrln(question);
115
+ for (let i = 0; i < choices.length; i++) {
116
+ stderrln(` ${i + 1}) ${choices[i]}`);
117
+ }
118
+ while (true) {
119
+ const raw = await ask("Enter choice: ");
120
+ const n = parseInt(raw, 10);
121
+ if (n >= 1 && n <= choices.length) {
122
+ return choices[n - 1]!;
123
+ }
124
+ stderrln(`Please enter a number between 1 and ${choices.length}.`);
125
+ }
126
+ }
127
+
128
+ // --- Config helpers ---
129
+
130
+ function resolveConfigPath(client: Client, scope: Scope): string {
131
+ if (client === "Claude Code") {
132
+ return scope === "Project"
133
+ ? path.join(process.cwd(), ".mcp.json")
134
+ : path.join(os.homedir(), ".mcp.json");
135
+ }
136
+ // Cursor
137
+ return scope === "Project"
138
+ ? path.join(process.cwd(), ".cursor", "mcp.json")
139
+ : path.join(os.homedir(), ".cursor", "mcp.json");
140
+ }
141
+
142
+ function buildEntry(token: string): Record<string, unknown> {
143
+ return {
144
+ type: "stdio",
145
+ command: "npx",
146
+ args: ["-y", "mcp-pachca@latest"],
147
+ env: {
148
+ PACHCA_ACCESS_TOKEN: token,
149
+ },
150
+ };
151
+ }
152
+
153
+ // --- Main ---
154
+
155
+ export async function runSetup(): Promise<void> {
156
+ stderrln("mcp-pachca setup wizard\n");
157
+
158
+ let client: Client;
159
+ let scope: Scope;
160
+ let token: string;
161
+
162
+ if (process.stdin.isTTY) {
163
+ const prompter = makeTTYPrompter();
164
+ client = await askChoice(prompter.ask, "Select your MCP client:", CLIENTS);
165
+ scope = await askChoice(prompter.ask, "Select config scope:", SCOPES);
166
+ prompter.close();
167
+ token = await askMaskedTTY("Enter your PACHCA_ACCESS_TOKEN: ");
168
+ } else {
169
+ const lines = await readAllLines();
170
+ const prompter = makePipePrompter(lines);
171
+ client = await askChoice(prompter.ask, "Select your MCP client:", CLIENTS);
172
+ scope = await askChoice(prompter.ask, "Select config scope:", SCOPES);
173
+ token = prompter.ask("Enter your PACHCA_ACCESS_TOKEN: ");
174
+ }
175
+
176
+ if (!token) {
177
+ stderrln("Error: token cannot be empty.");
178
+ process.exit(1);
179
+ }
180
+
181
+ const configPath = resolveConfigPath(client, scope);
182
+ const dir = path.dirname(configPath);
183
+
184
+ // Ensure parent directories exist
185
+ fs.mkdirSync(dir, { recursive: true });
186
+
187
+ // Read existing config or start fresh
188
+ let config: Record<string, unknown>;
189
+
190
+ if (fs.existsSync(configPath)) {
191
+ const raw = fs.readFileSync(configPath, "utf8");
192
+ try {
193
+ config = JSON.parse(raw) as Record<string, unknown>;
194
+ } catch {
195
+ stderrln(`Error: ${configPath} contains malformed JSON. Fix it manually before re-running setup.`);
196
+ process.exit(1);
197
+ }
198
+ } else {
199
+ config = {};
200
+ }
201
+
202
+ // Merge
203
+ const servers = (config["mcpServers"] ?? {}) as Record<string, unknown>;
204
+ servers["pachca"] = buildEntry(token);
205
+ config["mcpServers"] = servers;
206
+
207
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
208
+
209
+ stderrln(`\nConfig written to ${configPath}`);
210
+ stderrln("Restart your MCP client to apply changes.");
211
+ }
@@ -0,0 +1,138 @@
1
+ import type { PachcaClient } from "../client.js";
2
+ import type {
3
+ CursorPaginatedResponse,
4
+ PagePaginatedResponse,
5
+ SingleResponse,
6
+ PachcaUser,
7
+ PachcaChat,
8
+ PachcaMessage,
9
+ PachcaTag,
10
+ PachcaThread,
11
+ } from "./types.js";
12
+
13
+ type HandlerFn = (
14
+ client: PachcaClient,
15
+ args: Record<string, unknown>,
16
+ ) => Promise<unknown>;
17
+
18
+ async function handleListUsers(
19
+ client: PachcaClient,
20
+ args: Record<string, unknown>,
21
+ ): Promise<unknown> {
22
+ const params: Record<string, string> = {};
23
+ if (args["query"]) params["query"] = String(args["query"]);
24
+ if (args["limit"]) params["limit"] = String(args["limit"]);
25
+ if (args["cursor"]) params["cursor"] = String(args["cursor"]);
26
+ return client.get<CursorPaginatedResponse<PachcaUser>>("/users", params);
27
+ }
28
+
29
+ async function handleGetUser(
30
+ client: PachcaClient,
31
+ args: Record<string, unknown>,
32
+ ): Promise<unknown> {
33
+ return client.get<SingleResponse<PachcaUser>>(`/users/${args["id"]}`);
34
+ }
35
+
36
+ async function handleListChats(
37
+ client: PachcaClient,
38
+ args: Record<string, unknown>,
39
+ ): Promise<unknown> {
40
+ const params: Record<string, string> = {};
41
+ if (args["availability"]) params["availability"] = String(args["availability"]);
42
+ if (args["limit"]) params["limit"] = String(args["limit"]);
43
+ if (args["cursor"]) params["cursor"] = String(args["cursor"]);
44
+ if (args["personal"] !== undefined) params["personal"] = String(args["personal"]);
45
+ if (args["sort"]) {
46
+ const sort = String(args["sort"]);
47
+ if (sort.startsWith("id_")) {
48
+ params["sort[id]"] = sort.replace("id_", "");
49
+ } else if (sort.startsWith("last_message_at_")) {
50
+ params["sort[last_message_at]"] = sort.replace("last_message_at_", "");
51
+ }
52
+ }
53
+ return client.get<CursorPaginatedResponse<PachcaChat>>("/chats", params);
54
+ }
55
+
56
+ async function handleGetChat(
57
+ client: PachcaClient,
58
+ args: Record<string, unknown>,
59
+ ): Promise<unknown> {
60
+ return client.get<SingleResponse<PachcaChat>>(`/chats/${args["id"]}`);
61
+ }
62
+
63
+ async function handleGetChatMembers(
64
+ client: PachcaClient,
65
+ args: Record<string, unknown>,
66
+ ): Promise<unknown> {
67
+ const params: Record<string, string> = {};
68
+ if (args["role"]) params["role"] = String(args["role"]);
69
+ if (args["limit"]) params["limit"] = String(args["limit"]);
70
+ if (args["cursor"]) params["cursor"] = String(args["cursor"]);
71
+ return client.get<CursorPaginatedResponse<PachcaUser>>(
72
+ `/chats/${args["id"]}/members`,
73
+ params,
74
+ );
75
+ }
76
+
77
+ async function handleListMessages(
78
+ client: PachcaClient,
79
+ args: Record<string, unknown>,
80
+ ): Promise<unknown> {
81
+ const params: Record<string, string> = {
82
+ chat_id: String(args["chat_id"]),
83
+ };
84
+ if (args["per"]) params["per"] = String(args["per"]);
85
+ if (args["page"]) params["page"] = String(args["page"]);
86
+ return client.get<PagePaginatedResponse<PachcaMessage>>("/messages", params);
87
+ }
88
+
89
+ async function handleGetMessage(
90
+ client: PachcaClient,
91
+ args: Record<string, unknown>,
92
+ ): Promise<unknown> {
93
+ return client.get<SingleResponse<PachcaMessage>>(`/messages/${args["id"]}`);
94
+ }
95
+
96
+ async function handleListTags(
97
+ client: PachcaClient,
98
+ args: Record<string, unknown>,
99
+ ): Promise<unknown> {
100
+ const params: Record<string, string> = {};
101
+ if (args["per"]) params["per"] = String(args["per"]);
102
+ if (args["page"]) params["page"] = String(args["page"]);
103
+ return client.get<PagePaginatedResponse<PachcaTag>>("/group_tags", params);
104
+ }
105
+
106
+ async function handleGetThread(
107
+ client: PachcaClient,
108
+ args: Record<string, unknown>,
109
+ ): Promise<unknown> {
110
+ return client.get<SingleResponse<PachcaThread>>(`/threads/${args["id"]}`);
111
+ }
112
+
113
+ async function handleSendMessage(
114
+ client: PachcaClient,
115
+ args: Record<string, unknown>,
116
+ ): Promise<unknown> {
117
+ const message: Record<string, unknown> = {
118
+ entity_id: args["entity_id"],
119
+ content: args["content"],
120
+ };
121
+ if (args["entity_type"]) message["entity_type"] = args["entity_type"];
122
+ if (args["parent_message_id"])
123
+ message["parent_message_id"] = args["parent_message_id"];
124
+ return client.post<SingleResponse<PachcaMessage>>("/messages", { message });
125
+ }
126
+
127
+ export const handlerRegistry: Record<string, HandlerFn> = {
128
+ pachca_list_users: handleListUsers,
129
+ pachca_get_user: handleGetUser,
130
+ pachca_list_chats: handleListChats,
131
+ pachca_get_chat: handleGetChat,
132
+ pachca_get_chat_members: handleGetChatMembers,
133
+ pachca_list_messages: handleListMessages,
134
+ pachca_get_message: handleGetMessage,
135
+ pachca_list_tags: handleListTags,
136
+ pachca_get_thread: handleGetThread,
137
+ pachca_send_message: handleSendMessage,
138
+ };
@@ -0,0 +1,193 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+
3
+ export const toolDefinitions: Tool[] = [
4
+ {
5
+ name: "pachca_list_users",
6
+ description:
7
+ "List users (employees) in Pachca workspace. Supports search by name/email/nickname and cursor-based pagination.",
8
+ inputSchema: {
9
+ type: "object" as const,
10
+ properties: {
11
+ query: {
12
+ type: "string",
13
+ description:
14
+ "Search by first_name, last_name, email, phone_number, or nickname",
15
+ },
16
+ limit: {
17
+ type: "number",
18
+ description: "Results per page (max 50, default 50)",
19
+ },
20
+ cursor: {
21
+ type: "string",
22
+ description: "Pagination cursor from previous response",
23
+ },
24
+ },
25
+ },
26
+ },
27
+ {
28
+ name: "pachca_get_user",
29
+ description: "Get a single user by ID.",
30
+ inputSchema: {
31
+ type: "object" as const,
32
+ properties: {
33
+ id: { type: "number", description: "User ID" },
34
+ },
35
+ required: ["id"],
36
+ },
37
+ },
38
+ {
39
+ name: "pachca_list_chats",
40
+ description:
41
+ "List chats (discussions, channels) in Pachca. Supports filtering and cursor-based pagination.",
42
+ inputSchema: {
43
+ type: "object" as const,
44
+ properties: {
45
+ availability: {
46
+ type: "string",
47
+ enum: ["is_member", "public"],
48
+ description:
49
+ "Filter: is_member (default) = chats you belong to, public = public chats",
50
+ },
51
+ sort: {
52
+ type: "string",
53
+ enum: ["id_asc", "id_desc", "last_message_at_asc", "last_message_at_desc"],
54
+ description: "Sort order (default: last_message_at_desc)",
55
+ },
56
+ personal: {
57
+ type: "boolean",
58
+ description: "true = personal chats only, false = group chats only",
59
+ },
60
+ limit: {
61
+ type: "number",
62
+ description: "Results per page (max 50, default 50)",
63
+ },
64
+ cursor: {
65
+ type: "string",
66
+ description: "Pagination cursor from previous response",
67
+ },
68
+ },
69
+ },
70
+ },
71
+ {
72
+ name: "pachca_get_chat",
73
+ description: "Get a single chat by ID.",
74
+ inputSchema: {
75
+ type: "object" as const,
76
+ properties: {
77
+ id: { type: "number", description: "Chat ID" },
78
+ },
79
+ required: ["id"],
80
+ },
81
+ },
82
+ {
83
+ name: "pachca_get_chat_members",
84
+ description:
85
+ "Get members of a chat. Supports role filtering and cursor-based pagination.",
86
+ inputSchema: {
87
+ type: "object" as const,
88
+ properties: {
89
+ id: { type: "number", description: "Chat ID" },
90
+ role: {
91
+ type: "string",
92
+ enum: ["all", "owner", "admin", "editor", "member"],
93
+ description: "Filter by role (default: all)",
94
+ },
95
+ limit: {
96
+ type: "number",
97
+ description: "Results per page (max 50, default 50)",
98
+ },
99
+ cursor: {
100
+ type: "string",
101
+ description: "Pagination cursor from previous response",
102
+ },
103
+ },
104
+ required: ["id"],
105
+ },
106
+ },
107
+ {
108
+ name: "pachca_list_messages",
109
+ description:
110
+ "List messages in a chat. Uses page-based pagination. Returns newest messages first by default.",
111
+ inputSchema: {
112
+ type: "object" as const,
113
+ properties: {
114
+ chat_id: { type: "number", description: "Chat ID (required)" },
115
+ per: {
116
+ type: "number",
117
+ description: "Results per page (max 50, default 25)",
118
+ },
119
+ page: {
120
+ type: "number",
121
+ description: "Page number (default 1)",
122
+ },
123
+ },
124
+ required: ["chat_id"],
125
+ },
126
+ },
127
+ {
128
+ name: "pachca_get_message",
129
+ description: "Get a single message by ID.",
130
+ inputSchema: {
131
+ type: "object" as const,
132
+ properties: {
133
+ id: { type: "number", description: "Message ID" },
134
+ },
135
+ required: ["id"],
136
+ },
137
+ },
138
+ {
139
+ name: "pachca_list_tags",
140
+ description:
141
+ "List group tags in Pachca workspace. Uses page-based pagination.",
142
+ inputSchema: {
143
+ type: "object" as const,
144
+ properties: {
145
+ per: {
146
+ type: "number",
147
+ description: "Results per page (max 50, default 25)",
148
+ },
149
+ page: {
150
+ type: "number",
151
+ description: "Page number (default 1)",
152
+ },
153
+ },
154
+ },
155
+ },
156
+ {
157
+ name: "pachca_get_thread",
158
+ description: "Get thread details by ID.",
159
+ inputSchema: {
160
+ type: "object" as const,
161
+ properties: {
162
+ id: { type: "number", description: "Thread ID" },
163
+ },
164
+ required: ["id"],
165
+ },
166
+ },
167
+ {
168
+ name: "pachca_send_message",
169
+ description:
170
+ "Send a message to a chat, user, or thread in Pachca.",
171
+ inputSchema: {
172
+ type: "object" as const,
173
+ properties: {
174
+ entity_id: {
175
+ type: "number",
176
+ description: "Target ID: chat ID, user ID, or thread ID",
177
+ },
178
+ content: { type: "string", description: "Message text" },
179
+ entity_type: {
180
+ type: "string",
181
+ enum: ["discussion", "user", "thread"],
182
+ description:
183
+ "Target type: discussion (chat, default), user (DM), thread",
184
+ },
185
+ parent_message_id: {
186
+ type: "number",
187
+ description: "Reply to a specific message ID",
188
+ },
189
+ },
190
+ required: ["entity_id", "content"],
191
+ },
192
+ },
193
+ ];
@@ -0,0 +1,106 @@
1
+ export interface PachcaUser {
2
+ id: number;
3
+ first_name: string;
4
+ last_name: string;
5
+ nickname: string;
6
+ email: string;
7
+ phone_number: string | null;
8
+ department: string | null;
9
+ title: string | null;
10
+ role: "admin" | "user" | "multi_guest";
11
+ suspended: boolean;
12
+ invite_status: "confirmed" | "sent";
13
+ list_tags: string[];
14
+ custom_properties: PachcaCustomProperty[];
15
+ user_status: PachcaUserStatus | null;
16
+ bot: boolean;
17
+ sso: boolean;
18
+ created_at: string;
19
+ last_activity_at: string | null;
20
+ time_zone: string;
21
+ image_url: string | null;
22
+ }
23
+
24
+ export interface PachcaCustomProperty {
25
+ id: number;
26
+ name: string;
27
+ data_type: string;
28
+ value: string;
29
+ }
30
+
31
+ export interface PachcaUserStatus {
32
+ emoji: string;
33
+ title: string;
34
+ expires_at: string | null;
35
+ }
36
+
37
+ export interface PachcaChat {
38
+ id: number;
39
+ name: string;
40
+ owner_id: number;
41
+ created_at: string;
42
+ member_ids: number[];
43
+ group_tag_ids: number[];
44
+ channel: boolean;
45
+ personal: boolean;
46
+ public: boolean;
47
+ last_message_at: string | null;
48
+ meet_room_url: string | null;
49
+ }
50
+
51
+ export interface PachcaMessage {
52
+ id: number;
53
+ entity_type: "discussion" | "thread" | "user";
54
+ entity_id: number;
55
+ chat_id: number;
56
+ content: string;
57
+ user_id: number;
58
+ created_at: string;
59
+ url: string;
60
+ files: PachcaFile[];
61
+ thread: PachcaThreadRef | null;
62
+ parent_message_id: number | null;
63
+ }
64
+
65
+ export interface PachcaFile {
66
+ id: number;
67
+ key: string;
68
+ name: string;
69
+ file_type: "file" | "image";
70
+ url: string;
71
+ }
72
+
73
+ export interface PachcaThreadRef {
74
+ id: number;
75
+ chat_id: number;
76
+ message_id: number;
77
+ message_chat_id: number;
78
+ updated_at: string;
79
+ }
80
+
81
+ export interface PachcaThread {
82
+ id: number;
83
+ chat_id: number;
84
+ message_id: number;
85
+ message_chat_id: number;
86
+ updated_at: string;
87
+ }
88
+
89
+ export interface PachcaTag {
90
+ id: number;
91
+ name: string;
92
+ users_count: number;
93
+ }
94
+
95
+ export interface CursorPaginatedResponse<T> {
96
+ meta: { paginate: { next_page: string | null } };
97
+ data: T[];
98
+ }
99
+
100
+ export interface PagePaginatedResponse<T> {
101
+ data: T[];
102
+ }
103
+
104
+ export interface SingleResponse<T> {
105
+ data: T;
106
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "sourceMap": true,
14
+ "isolatedModules": true,
15
+ "outDir": "dist",
16
+ "rootDir": "src"
17
+ },
18
+ "include": ["src"]
19
+ }