habitica-mcp 0.0.1-alpha.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/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # habitica-mcp
2
+
3
+ Habitica Model Context Protocol server built with Effect v4 beta.
4
+
5
+ The server exposes typed Habitica read/write tools over stdio. Tool handlers
6
+ depend on an Effect `HabiticaGateway` port; the live adapter uses Effect HTTP
7
+ and schema-decodes Habitica API responses at the boundary.
8
+
9
+ ## Requirements
10
+
11
+ - Node.js `>=22.12.0`
12
+ - pnpm `>=10`
13
+
14
+ This repo uses pnpm rather than bun because the server runs on Node stdio, the
15
+ lockfile is already deterministic, and the Effect MCP docs target Node runtime
16
+ primitives.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ pnpm add -g habitica-mcp@alpha
22
+ ```
23
+
24
+ For local development:
25
+
26
+ ```sh
27
+ pnpm install
28
+ ```
29
+
30
+ Required variables:
31
+
32
+ - `HABITICA_USER_ID`
33
+ - `HABITICA_API_TOKEN`
34
+ - `HABITICA_CLIENT_ID`
35
+ - `HABITICA_API_BASE_URL` defaults to `https://habitica.com/api/v3`
36
+
37
+ For a local checkout, copy the example env file and fill in your Habitica credentials:
38
+
39
+ ```sh
40
+ cp .env.example .env
41
+ ```
42
+
43
+ ## Commands
44
+
45
+ ```sh
46
+ pnpm dev # run the stdio MCP server from TypeScript
47
+ pnpm build # emit dist
48
+ pnpm check # full deterministic gate used by Lefthook
49
+ pnpm test # run unit tests
50
+ pnpm test:coverage # run unit tests with 100% coverage threshold
51
+ pnpm e2e # run strict effect-bdd Gherkin tests
52
+ pnpm mutation # run Stryker with 100% mutation threshold
53
+ ```
54
+
55
+ ## Deterministic Gate
56
+
57
+ `pnpm check` runs build, typecheck, suppression policy, deterministic scope
58
+ policy, custom oxlint RuleTester coverage, oxlint, format check, 100% unit
59
+ coverage, strict `effect-bdd` Gherkin e2e, 100% Stryker mutation coverage for
60
+ the deterministic core, and `knip`.
61
+
62
+ Lefthook runs `pnpm check` on pre-commit:
63
+
64
+ ```sh
65
+ pnpm prepare
66
+ ```
67
+
68
+ GitHub Actions runs the same `pnpm check` gate on pushes to `main` and pull
69
+ requests.
70
+
71
+ ## Tool Surface
72
+
73
+ `HelloWorldTool` returns a deterministic greeting and does not require Habitica
74
+ credentials. Use it as the first MCP smoke test.
75
+
76
+ Core tools cover profile, stats, tasks, tags, checklists, and notifications.
77
+ Expanded tools cover rewards, inventory, shop items, pets, mounts, and skills.
78
+
79
+ Mutating tools use explicit verb names such as `CreateTaskTool`,
80
+ `UpdateTaskTool`, `DeleteTaskTool`, `ScoreTaskTool`, `ReadNotificationTool`,
81
+ `BuyRewardTool`, and `CastSkillTool`. They request approval and return typed
82
+ structured results.
83
+
84
+ ## Architecture Guardrails
85
+
86
+ - MCP stdout is protocol-owned; logs go to stderr.
87
+ - Tools import `HabiticaGateway`, not `HabiticaHttpAdapter` or raw route
88
+ strings.
89
+ - Habitica credentials and auth headers must never be logged.
90
+ - Every `Tool.make` call declares a success schema.
91
+ - Deterministic modules must be listed in coverage and mutation scope.
92
+
93
+ ## MCP Config
94
+
95
+ Use the local TypeScript entrypoint while developing:
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "habitica": {
101
+ "command": "pnpm",
102
+ "args": ["--dir", "/absolute/path/to/habitica-mcp", "dev"]
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ After `pnpm build`, use the package binary:
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "habitica": {
114
+ "command": "node",
115
+ "args": ["/absolute/path/to/habitica-mcp/dist/main.js"]
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ After installing from npm, use the binary:
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "habitica": {
127
+ "command": "habitica-mcp"
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ ## Publishing
134
+
135
+ This package is intentionally pre-1.0. Publish early builds with the manual
136
+ `Publish` GitHub Actions workflow. It uses the repository `NPM_TOKEN` secret,
137
+ runs `pnpm check`, and publishes with npm provenance on the `alpha` dist-tag.
138
+
139
+ Equivalent local command:
140
+
141
+ ```sh
142
+ pnpm check
143
+ npm publish --tag alpha --provenance
144
+ ```
145
+
146
+ `prepack` builds `dist/`; `publishConfig` marks the package public and enables npm provenance.
@@ -0,0 +1 @@
1
+ export declare const run: () => void;
@@ -0,0 +1,17 @@
1
+ import { NodeRuntime, NodeStdio } from "@effect/platform-node";
2
+ import { Layer, Logger } from "effect";
3
+ import { McpServer } from "effect/unstable/ai";
4
+ import { HabiticaConfig } from "./config/HabiticaConfig.js";
5
+ import { HabiticaHttpAdapter } from "./habitica/HabiticaHttpAdapter.js";
6
+ import { DailyPlanningPrompt, HabitCheckInPrompt, TaskReviewPrompt, } from "./prompts/HabiticaPrompts.js";
7
+ import { CapabilitiesResource, TaskTemplateResource } from "./resources/HabiticaResources.js";
8
+ import { HabiticaToolLayer, HabiticaToolkit } from "./tools/HabiticaTools.js";
9
+ const HabiticaToolkitLayer = McpServer.toolkit(HabiticaToolkit).pipe(Layer.provideMerge(HabiticaToolLayer.pipe(Layer.provide(HabiticaHttpAdapter.gatewayLayer), Layer.provide(HabiticaConfig.layer))));
10
+ const HabiticaMcpPartsLayer = Layer.mergeAll(CapabilitiesResource, TaskTemplateResource, DailyPlanningPrompt, HabitCheckInPrompt, TaskReviewPrompt, HabiticaToolkitLayer);
11
+ const ServerLayer = HabiticaMcpPartsLayer.pipe(Layer.provide(McpServer.layerStdio({
12
+ name: "Habitica MCP",
13
+ version: "0.0.1-alpha.0",
14
+ })), Layer.provide(NodeStdio.layer), Layer.provide(Layer.succeed(Logger.LogToStderr)(true)));
15
+ export const run = () => {
16
+ Layer.launch(ServerLayer).pipe(NodeRuntime.runMain);
17
+ };
@@ -0,0 +1,13 @@
1
+ import { Config, Context, Layer } from "effect";
2
+ export interface HabiticaConfigShape {
3
+ readonly apiBaseUrl: string;
4
+ readonly apiToken: string;
5
+ readonly clientId: string;
6
+ readonly userId: string;
7
+ }
8
+ declare const HabiticaConfig_base: Context.ServiceClass<HabiticaConfig, "habitica-mcp/HabiticaConfig", HabiticaConfigShape>;
9
+ export declare class HabiticaConfig extends HabiticaConfig_base {
10
+ static readonly layer: Layer.Layer<HabiticaConfig, Config.ConfigError, never>;
11
+ static readonly from: (config: HabiticaConfigShape) => Layer.Layer<HabiticaConfig, never, never>;
12
+ }
13
+ export {};
@@ -0,0 +1,16 @@
1
+ import { Config, Context, Effect, Layer, Redacted } from "effect";
2
+ export class HabiticaConfig extends Context.Service()("habitica-mcp/HabiticaConfig") {
3
+ static layer = Layer.effect(HabiticaConfig, Effect.gen(function* () {
4
+ const userId = yield* Config.string("HABITICA_USER_ID");
5
+ const apiToken = yield* Config.redacted("HABITICA_API_TOKEN");
6
+ const clientId = yield* Config.string("HABITICA_CLIENT_ID");
7
+ const apiBaseUrl = yield* Config.string("HABITICA_API_BASE_URL").pipe(Config.withDefault("https://habitica.com/api/v3"));
8
+ return HabiticaConfig.of({
9
+ apiBaseUrl,
10
+ apiToken: Redacted.value(apiToken),
11
+ clientId,
12
+ userId,
13
+ });
14
+ }));
15
+ static from = (config) => Layer.succeed(HabiticaConfig)(HabiticaConfig.of(config));
16
+ }
@@ -0,0 +1,35 @@
1
+ import { Schema } from "effect";
2
+ declare const HabiticaConfigError_base: Schema.Class<HabiticaConfigError, Schema.TaggedStruct<"HabiticaConfigError", {
3
+ readonly message: Schema.String;
4
+ }>, import("effect/Cause").YieldableError>;
5
+ declare class HabiticaConfigError extends HabiticaConfigError_base {
6
+ }
7
+ declare const HabiticaAuthError_base: Schema.Class<HabiticaAuthError, Schema.TaggedStruct<"HabiticaAuthError", {
8
+ readonly message: Schema.String;
9
+ }>, import("effect/Cause").YieldableError>;
10
+ export declare class HabiticaAuthError extends HabiticaAuthError_base {
11
+ }
12
+ declare const HabiticaRateLimitError_base: Schema.Class<HabiticaRateLimitError, Schema.TaggedStruct<"HabiticaRateLimitError", {
13
+ readonly message: Schema.String;
14
+ }>, import("effect/Cause").YieldableError>;
15
+ export declare class HabiticaRateLimitError extends HabiticaRateLimitError_base {
16
+ }
17
+ declare const HabiticaNotFoundError_base: Schema.Class<HabiticaNotFoundError, Schema.TaggedStruct<"HabiticaNotFoundError", {
18
+ readonly message: Schema.String;
19
+ }>, import("effect/Cause").YieldableError>;
20
+ export declare class HabiticaNotFoundError extends HabiticaNotFoundError_base {
21
+ }
22
+ declare const HabiticaApiError_base: Schema.Class<HabiticaApiError, Schema.TaggedStruct<"HabiticaApiError", {
23
+ readonly message: Schema.String;
24
+ readonly status: Schema.optional<Schema.Number>;
25
+ }>, import("effect/Cause").YieldableError>;
26
+ export declare class HabiticaApiError extends HabiticaApiError_base {
27
+ }
28
+ declare const HabiticaDecodeError_base: Schema.Class<HabiticaDecodeError, Schema.TaggedStruct<"HabiticaDecodeError", {
29
+ readonly message: Schema.String;
30
+ }>, import("effect/Cause").YieldableError>;
31
+ export declare class HabiticaDecodeError extends HabiticaDecodeError_base {
32
+ }
33
+ export type HabiticaError = HabiticaApiError | HabiticaAuthError | HabiticaConfigError | HabiticaDecodeError | HabiticaNotFoundError | HabiticaRateLimitError;
34
+ export declare const HabiticaErrorSchema: Schema.Union<readonly [typeof HabiticaApiError, typeof HabiticaAuthError, typeof HabiticaConfigError, typeof HabiticaDecodeError, typeof HabiticaNotFoundError, typeof HabiticaRateLimitError]>;
35
+ export {};
@@ -0,0 +1,34 @@
1
+ import { Schema } from "effect";
2
+ class HabiticaConfigError extends Schema.TaggedErrorClass()("HabiticaConfigError", {
3
+ message: Schema.String,
4
+ }) {
5
+ }
6
+ export class HabiticaAuthError extends Schema.TaggedErrorClass()("HabiticaAuthError", {
7
+ message: Schema.String,
8
+ }) {
9
+ }
10
+ export class HabiticaRateLimitError extends Schema.TaggedErrorClass()("HabiticaRateLimitError", {
11
+ message: Schema.String,
12
+ }) {
13
+ }
14
+ export class HabiticaNotFoundError extends Schema.TaggedErrorClass()("HabiticaNotFoundError", {
15
+ message: Schema.String,
16
+ }) {
17
+ }
18
+ export class HabiticaApiError extends Schema.TaggedErrorClass()("HabiticaApiError", {
19
+ message: Schema.String,
20
+ status: Schema.optional(Schema.Number),
21
+ }) {
22
+ }
23
+ export class HabiticaDecodeError extends Schema.TaggedErrorClass()("HabiticaDecodeError", {
24
+ message: Schema.String,
25
+ }) {
26
+ }
27
+ export const HabiticaErrorSchema = Schema.Union([
28
+ HabiticaApiError,
29
+ HabiticaAuthError,
30
+ HabiticaConfigError,
31
+ HabiticaDecodeError,
32
+ HabiticaNotFoundError,
33
+ HabiticaRateLimitError,
34
+ ]);
@@ -0,0 +1,77 @@
1
+ import { Context, type Effect } from "effect";
2
+ import type { HabiticaError } from "./HabiticaErrors.js";
3
+ import type { CreateTagInput, CreateTaskInput, Direction, HabiticaInventory, HabiticaMutationResult, HabiticaNotification, HabiticaProfile, HabiticaShopItem, HabiticaSkill, HabiticaTag, HabiticaTask, TaskType, UpdateChecklistItemInput, UpdateTaskInput } from "./HabiticaSchemas.js";
4
+ export interface HabiticaGatewayShape {
5
+ readonly addChecklistItem: (input: {
6
+ readonly taskId: string;
7
+ readonly text: string;
8
+ }) => Effect.Effect<HabiticaTask, HabiticaError>;
9
+ readonly buyReward: (input: {
10
+ readonly rewardId: string;
11
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
12
+ readonly buyShopItem: (input: {
13
+ readonly key: string;
14
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
15
+ readonly castSkill: (input: {
16
+ readonly skillKey: string;
17
+ readonly targetId?: string | undefined;
18
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
19
+ readonly createReward: (input: CreateTaskInput) => Effect.Effect<HabiticaTask, HabiticaError>;
20
+ readonly createTag: (input: CreateTagInput) => Effect.Effect<HabiticaTag, HabiticaError>;
21
+ readonly createTask: (input: CreateTaskInput) => Effect.Effect<HabiticaTask, HabiticaError>;
22
+ readonly deleteChecklistItem: (input: {
23
+ readonly itemId: string;
24
+ readonly taskId: string;
25
+ }) => Effect.Effect<HabiticaTask, HabiticaError>;
26
+ readonly deleteReward: (input: {
27
+ readonly rewardId: string;
28
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
29
+ readonly deleteTask: (input: {
30
+ readonly taskId: string;
31
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
32
+ readonly equipMount: (input: {
33
+ readonly mountKey: string;
34
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
35
+ readonly equipPet: (input: {
36
+ readonly petKey: string;
37
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
38
+ readonly feedPet: (input: {
39
+ readonly foodKey: string;
40
+ readonly petKey: string;
41
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
42
+ readonly getInventory: Effect.Effect<HabiticaInventory, HabiticaError>;
43
+ readonly getStats: Effect.Effect<HabiticaProfile["stats"], HabiticaError>;
44
+ readonly getTask: (input: {
45
+ readonly taskId: string;
46
+ }) => Effect.Effect<HabiticaTask, HabiticaError>;
47
+ readonly getUserProfile: Effect.Effect<HabiticaProfile, HabiticaError>;
48
+ readonly hatchPet: (input: {
49
+ readonly eggKey: string;
50
+ readonly hatchingPotionKey: string;
51
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
52
+ readonly listNotifications: Effect.Effect<ReadonlyArray<HabiticaNotification>, HabiticaError>;
53
+ readonly listShopItems: Effect.Effect<ReadonlyArray<HabiticaShopItem>, HabiticaError>;
54
+ readonly listSkills: Effect.Effect<ReadonlyArray<HabiticaSkill>, HabiticaError>;
55
+ readonly listTags: Effect.Effect<ReadonlyArray<HabiticaTag>, HabiticaError>;
56
+ readonly listTasks: (input: {
57
+ readonly type?: TaskType | undefined;
58
+ }) => Effect.Effect<ReadonlyArray<HabiticaTask>, HabiticaError>;
59
+ readonly readNotification: (input: {
60
+ readonly notificationId: string;
61
+ }) => Effect.Effect<HabiticaMutationResult, HabiticaError>;
62
+ readonly scoreChecklistItem: (input: {
63
+ readonly itemId: string;
64
+ readonly taskId: string;
65
+ }) => Effect.Effect<HabiticaTask, HabiticaError>;
66
+ readonly scoreTask: (input: {
67
+ readonly direction: Direction;
68
+ readonly taskId: string;
69
+ }) => Effect.Effect<HabiticaTask, HabiticaError>;
70
+ readonly updateChecklistItem: (input: UpdateChecklistItemInput) => Effect.Effect<HabiticaTask, HabiticaError>;
71
+ readonly updateReward: (input: UpdateTaskInput) => Effect.Effect<HabiticaTask, HabiticaError>;
72
+ readonly updateTask: (input: UpdateTaskInput) => Effect.Effect<HabiticaTask, HabiticaError>;
73
+ }
74
+ declare const HabiticaGateway_base: Context.ServiceClass<HabiticaGateway, "habitica-mcp/HabiticaGateway", HabiticaGatewayShape>;
75
+ export declare class HabiticaGateway extends HabiticaGateway_base {
76
+ }
77
+ export {};
@@ -0,0 +1,3 @@
1
+ import { Context } from "effect";
2
+ export class HabiticaGateway extends Context.Service()("habitica-mcp/HabiticaGateway") {
3
+ }
@@ -0,0 +1,8 @@
1
+ import { Layer } from "effect";
2
+ import { HabiticaConfig } from "../config/HabiticaConfig.js";
3
+ import { HabiticaGateway } from "./HabiticaGateway.js";
4
+ import { HabiticaTransport } from "./HabiticaTransport.js";
5
+ export declare const HabiticaHttpAdapter: {
6
+ readonly gatewayLayer: Layer.Layer<HabiticaGateway, never, HabiticaConfig>;
7
+ readonly transportLayer: Layer.Layer<HabiticaTransport, never, HabiticaConfig>;
8
+ };
@@ -0,0 +1,106 @@
1
+ import { Effect, flow, Layer, Schema } from "effect";
2
+ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http";
3
+ import { HabiticaConfig } from "../config/HabiticaConfig.js";
4
+ import { HabiticaApiError, HabiticaAuthError, HabiticaDecodeError, HabiticaNotFoundError, HabiticaRateLimitError, } from "./HabiticaErrors.js";
5
+ import { HabiticaGateway } from "./HabiticaGateway.js";
6
+ import { HabiticaRoutes, taskListUrlParams } from "./HabiticaRoutes.js";
7
+ import { CreateTaskInput, HabiticaInventory, HabiticaMutationResult, HabiticaNotification, HabiticaProfile, HabiticaShopItem, HabiticaSkill, HabiticaTag, HabiticaTask, } from "./HabiticaSchemas.js";
8
+ import { HabiticaTransport } from "./HabiticaTransport.js";
9
+ const responseData = (schema) => Schema.Struct({
10
+ data: schema,
11
+ success: Schema.Boolean,
12
+ });
13
+ const decodeUnknown = (schema, value) => Effect.try({
14
+ try: () => Schema.decodeUnknownSync(schema)(value),
15
+ catch: () => new HabiticaDecodeError({
16
+ message: "Habitica response did not match the expected schema.",
17
+ }),
18
+ });
19
+ const decodeData = (schema) => (value) => decodeUnknown(responseData(schema), value).pipe(Effect.map((body) => body.data));
20
+ const methodRequest = (request) => {
21
+ const requestForMethod = request.method === "GET"
22
+ ? HttpClientRequest.get(request.path)
23
+ : request.method === "POST"
24
+ ? HttpClientRequest.post(request.path)
25
+ : request.method === "PUT"
26
+ ? HttpClientRequest.put(request.path)
27
+ : HttpClientRequest.delete(request.path);
28
+ const withParams = request.urlParams === undefined
29
+ ? requestForMethod
30
+ : requestForMethod.pipe(HttpClientRequest.setUrlParams(request.urlParams));
31
+ return request.body === undefined
32
+ ? withParams
33
+ : withParams.pipe(HttpClientRequest.bodyJsonUnsafe(request.body));
34
+ };
35
+ const errorForStatus = (status) => status === 401 || status === 403
36
+ ? new HabiticaAuthError({ message: "Habitica rejected the configured credentials." })
37
+ : status === 404
38
+ ? new HabiticaNotFoundError({ message: "Habitica resource was not found." })
39
+ : status === 429
40
+ ? new HabiticaRateLimitError({ message: "Habitica rate limit exceeded." })
41
+ : new HabiticaApiError({ message: "Habitica API request failed.", status });
42
+ const ensureOk = (response) => response.status >= 200 && response.status < 300
43
+ ? Effect.succeed(response)
44
+ : Effect.fail(errorForStatus(response.status));
45
+ const rewardInput = (input) => input.notes === undefined
46
+ ? new CreateTaskInput({ text: input.text, type: "reward" })
47
+ : new CreateTaskInput({ notes: input.notes, text: input.text, type: "reward" });
48
+ const transportLayer = Layer.effect(HabiticaTransport, Effect.gen(function* () {
49
+ const config = yield* HabiticaConfig;
50
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.mapRequest(flow(HttpClientRequest.prependUrl(config.apiBaseUrl), HttpClientRequest.acceptJson, HttpClientRequest.setHeaders({
51
+ "x-api-key": config.apiToken,
52
+ "x-api-user": config.userId,
53
+ "x-client": config.clientId,
54
+ }))));
55
+ return HabiticaTransport.of({
56
+ request: (request, decode) => client.execute(methodRequest(request)).pipe(Effect.flatMap(ensureOk), Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Unknown)), Effect.flatMap(decode), Effect.mapError((error) => error instanceof HabiticaAuthError ||
57
+ error instanceof HabiticaApiError ||
58
+ error instanceof HabiticaDecodeError ||
59
+ error instanceof HabiticaNotFoundError ||
60
+ error instanceof HabiticaRateLimitError
61
+ ? error
62
+ : new HabiticaApiError({ message: "Habitica HTTP transport failed." }))),
63
+ });
64
+ })).pipe(Layer.provide(FetchHttpClient.layer));
65
+ const gatewayLayer = Layer.effect(HabiticaGateway, Effect.gen(function* () {
66
+ const transport = yield* HabiticaTransport;
67
+ const get = (path, schema, urlParams) => transport.request(urlParams === undefined ? { method: "GET", path } : { method: "GET", path, urlParams }, decodeData(schema));
68
+ const post = (path, body, schema) => transport.request({ body, method: "POST", path }, decodeData(schema));
69
+ const put = (path, body, schema) => transport.request({ body, method: "PUT", path }, decodeData(schema));
70
+ const del = (path, schema) => transport.request({ method: "DELETE", path }, decodeData(schema));
71
+ return HabiticaGateway.of({
72
+ addChecklistItem: ({ taskId, text }) => post(HabiticaRoutes.checklist(taskId), { text }, HabiticaTask),
73
+ buyReward: ({ rewardId }) => post(HabiticaRoutes.taskScore(rewardId, "down"), {}, HabiticaMutationResult),
74
+ buyShopItem: ({ key }) => post(HabiticaRoutes.buySpecialSpell(key), {}, HabiticaMutationResult),
75
+ castSkill: ({ skillKey, targetId }) => post(HabiticaRoutes.castSkill(skillKey), { targetId }, HabiticaMutationResult),
76
+ createReward: (input) => post(HabiticaRoutes.tasksUser(), rewardInput(input), HabiticaTask),
77
+ createTag: (input) => post(HabiticaRoutes.tags(), input, HabiticaTag),
78
+ createTask: (input) => post(HabiticaRoutes.tasksUser(), input, HabiticaTask),
79
+ deleteChecklistItem: ({ itemId, taskId }) => del(HabiticaRoutes.checklistItem(taskId, itemId), HabiticaTask),
80
+ deleteReward: ({ rewardId }) => del(HabiticaRoutes.task(rewardId), HabiticaMutationResult),
81
+ deleteTask: ({ taskId }) => del(HabiticaRoutes.task(taskId), HabiticaMutationResult),
82
+ equipMount: ({ mountKey }) => post(HabiticaRoutes.equipMount(mountKey), {}, HabiticaMutationResult),
83
+ equipPet: ({ petKey }) => post(HabiticaRoutes.equipPet(petKey), {}, HabiticaMutationResult),
84
+ feedPet: ({ foodKey, petKey }) => post(HabiticaRoutes.feedPet(petKey, foodKey), {}, HabiticaMutationResult),
85
+ getInventory: get(HabiticaRoutes.inventory(), HabiticaInventory),
86
+ getStats: get(HabiticaRoutes.user(), HabiticaProfile).pipe(Effect.map((profile) => profile.stats)),
87
+ getTask: ({ taskId }) => get(HabiticaRoutes.task(taskId), HabiticaTask),
88
+ getUserProfile: get(HabiticaRoutes.user(), HabiticaProfile),
89
+ hatchPet: ({ eggKey, hatchingPotionKey }) => post(HabiticaRoutes.hatchPet(eggKey, hatchingPotionKey), {}, HabiticaMutationResult),
90
+ listNotifications: get(HabiticaRoutes.notifications(), Schema.Array(HabiticaNotification)),
91
+ listShopItems: get(HabiticaRoutes.market(), Schema.Array(HabiticaShopItem)),
92
+ listSkills: get(HabiticaRoutes.skillList(), Schema.Array(HabiticaSkill)),
93
+ listTags: get(HabiticaRoutes.tags(), Schema.Array(HabiticaTag)),
94
+ listTasks: (input) => get(HabiticaRoutes.tasksUser(), Schema.Array(HabiticaTask), taskListUrlParams(input.type)),
95
+ readNotification: ({ notificationId }) => post(HabiticaRoutes.notificationRead(notificationId), {}, HabiticaMutationResult),
96
+ scoreChecklistItem: ({ itemId, taskId }) => post(HabiticaRoutes.checklistItemScore(taskId, itemId), {}, HabiticaTask),
97
+ scoreTask: ({ direction, taskId, }) => post(HabiticaRoutes.taskScore(taskId, direction), {}, HabiticaTask),
98
+ updateChecklistItem: (input) => put(HabiticaRoutes.checklistItem(input.taskId, input.itemId), input, HabiticaTask),
99
+ updateReward: (input) => put(HabiticaRoutes.task(input.id), input, HabiticaTask),
100
+ updateTask: (input) => put(HabiticaRoutes.task(input.id), input, HabiticaTask),
101
+ });
102
+ })).pipe(Layer.provide(transportLayer));
103
+ export const HabiticaHttpAdapter = {
104
+ gatewayLayer,
105
+ transportLayer,
106
+ };
@@ -0,0 +1,23 @@
1
+ import type { Direction, TaskType } from "./HabiticaSchemas.js";
2
+ export declare const HabiticaRoutes: {
3
+ buySpecialSpell: (key: string) => string;
4
+ castSkill: (skillKey: string) => string;
5
+ checklist: (taskId: string) => string;
6
+ checklistItem: (taskId: string, itemId: string) => string;
7
+ checklistItemScore: (taskId: string, itemId: string) => string;
8
+ equipMount: (mountKey: string) => string;
9
+ equipPet: (petKey: string) => string;
10
+ feedPet: (petKey: string, foodKey: string) => string;
11
+ hatchPet: (eggKey: string, hatchingPotionKey: string) => string;
12
+ inventory: () => string;
13
+ market: () => string;
14
+ notificationRead: (notificationId: string) => string;
15
+ notifications: () => string;
16
+ skillList: () => string;
17
+ tags: () => string;
18
+ task: (taskId: string) => string;
19
+ taskScore: (taskId: string, direction: Direction) => string;
20
+ tasksUser: () => string;
21
+ user: () => string;
22
+ };
23
+ export declare const taskListUrlParams: (type: TaskType | undefined) => Readonly<Record<string, string>> | undefined;
@@ -0,0 +1,22 @@
1
+ export const HabiticaRoutes = {
2
+ buySpecialSpell: (key) => `/user/buy-special-spell/${key}`,
3
+ castSkill: (skillKey) => `/user/class/cast/${skillKey}`,
4
+ checklist: (taskId) => `/tasks/${taskId}/checklist`,
5
+ checklistItem: (taskId, itemId) => `/tasks/${taskId}/checklist/${itemId}`,
6
+ checklistItemScore: (taskId, itemId) => `/tasks/${taskId}/checklist/${itemId}/score`,
7
+ equipMount: (mountKey) => `/user/equip/mount/${mountKey}`,
8
+ equipPet: (petKey) => `/user/equip/pet/${petKey}`,
9
+ feedPet: (petKey, foodKey) => `/user/feed/${petKey}/${foodKey}`,
10
+ hatchPet: (eggKey, hatchingPotionKey) => `/user/hatch/${eggKey}/${hatchingPotionKey}`,
11
+ inventory: () => "/user/inventory",
12
+ market: () => "/shops/market",
13
+ notificationRead: (notificationId) => `/notifications/${notificationId}/read`,
14
+ notifications: () => "/notifications",
15
+ skillList: () => "/user/class/cast",
16
+ tags: () => "/tags",
17
+ task: (taskId) => `/tasks/${taskId}`,
18
+ taskScore: (taskId, direction) => `/tasks/${taskId}/score/${direction}`,
19
+ tasksUser: () => "/tasks/user",
20
+ user: () => "/user",
21
+ };
22
+ export const taskListUrlParams = (type) => (type === undefined ? undefined : { type });
@@ -0,0 +1,111 @@
1
+ import { Schema } from "effect";
2
+ export declare const TaskType: Schema.Literals<readonly ["habit", "daily", "todo", "reward"]>;
3
+ export type TaskType = typeof TaskType.Type;
4
+ export declare const Direction: Schema.Literals<readonly ["up", "down"]>;
5
+ export type Direction = typeof Direction.Type;
6
+ declare const HabiticaStats_base: Schema.Class<HabiticaStats, Schema.Struct<{
7
+ readonly class: Schema.optional<Schema.String>;
8
+ readonly gp: Schema.Number;
9
+ readonly hp: Schema.Number;
10
+ readonly lvl: Schema.Number;
11
+ readonly mp: Schema.Number;
12
+ readonly toNextLevel: Schema.optional<Schema.Number>;
13
+ }>, {}>;
14
+ declare class HabiticaStats extends HabiticaStats_base {
15
+ }
16
+ declare const HabiticaProfile_base: Schema.Class<HabiticaProfile, Schema.Struct<{
17
+ readonly id: Schema.String;
18
+ readonly displayName: Schema.String;
19
+ readonly stats: typeof HabiticaStats;
20
+ }>, {}>;
21
+ export declare class HabiticaProfile extends HabiticaProfile_base {
22
+ }
23
+ declare const HabiticaChecklistItem_base: Schema.Class<HabiticaChecklistItem, Schema.Struct<{
24
+ readonly completed: Schema.Boolean;
25
+ readonly id: Schema.String;
26
+ readonly text: Schema.String;
27
+ }>, {}>;
28
+ export declare class HabiticaChecklistItem extends HabiticaChecklistItem_base {
29
+ }
30
+ declare const HabiticaTask_base: Schema.Class<HabiticaTask, Schema.Struct<{
31
+ readonly checklist: Schema.optional<Schema.$Array<typeof HabiticaChecklistItem>>;
32
+ readonly completed: Schema.optional<Schema.Boolean>;
33
+ readonly id: Schema.String;
34
+ readonly notes: Schema.optional<Schema.String>;
35
+ readonly text: Schema.String;
36
+ readonly type: Schema.Literals<readonly ["habit", "daily", "todo", "reward"]>;
37
+ }>, {}>;
38
+ export declare class HabiticaTask extends HabiticaTask_base {
39
+ }
40
+ declare const HabiticaTag_base: Schema.Class<HabiticaTag, Schema.Struct<{
41
+ readonly id: Schema.String;
42
+ readonly name: Schema.String;
43
+ }>, {}>;
44
+ export declare class HabiticaTag extends HabiticaTag_base {
45
+ }
46
+ declare const HabiticaNotification_base: Schema.Class<HabiticaNotification, Schema.Struct<{
47
+ readonly id: Schema.String;
48
+ readonly seen: Schema.Boolean;
49
+ readonly text: Schema.String;
50
+ readonly type: Schema.String;
51
+ }>, {}>;
52
+ export declare class HabiticaNotification extends HabiticaNotification_base {
53
+ }
54
+ declare const HabiticaInventory_base: Schema.Class<HabiticaInventory, Schema.Struct<{
55
+ readonly eggs: Schema.$Record<Schema.String, Schema.Number>;
56
+ readonly food: Schema.$Record<Schema.String, Schema.Number>;
57
+ readonly hatchingPotions: Schema.$Record<Schema.String, Schema.Number>;
58
+ readonly mounts: Schema.$Record<Schema.String, Schema.Boolean>;
59
+ readonly pets: Schema.$Record<Schema.String, Schema.Number>;
60
+ }>, {}>;
61
+ export declare class HabiticaInventory extends HabiticaInventory_base {
62
+ }
63
+ declare const HabiticaShopItem_base: Schema.Class<HabiticaShopItem, Schema.Struct<{
64
+ readonly key: Schema.String;
65
+ readonly text: Schema.String;
66
+ readonly value: Schema.Number;
67
+ }>, {}>;
68
+ export declare class HabiticaShopItem extends HabiticaShopItem_base {
69
+ }
70
+ declare const HabiticaSkill_base: Schema.Class<HabiticaSkill, Schema.Struct<{
71
+ readonly key: Schema.String;
72
+ readonly mana: Schema.Number;
73
+ readonly text: Schema.String;
74
+ }>, {}>;
75
+ export declare class HabiticaSkill extends HabiticaSkill_base {
76
+ }
77
+ declare const HabiticaMutationResult_base: Schema.Class<HabiticaMutationResult, Schema.Struct<{
78
+ readonly id: Schema.String;
79
+ readonly message: Schema.String;
80
+ }>, {}>;
81
+ export declare class HabiticaMutationResult extends HabiticaMutationResult_base {
82
+ }
83
+ declare const CreateTaskInput_base: Schema.Class<CreateTaskInput, Schema.Struct<{
84
+ readonly notes: Schema.optional<Schema.String>;
85
+ readonly text: Schema.String;
86
+ readonly type: Schema.Literals<readonly ["habit", "daily", "todo", "reward"]>;
87
+ }>, {}>;
88
+ export declare class CreateTaskInput extends CreateTaskInput_base {
89
+ }
90
+ declare const UpdateTaskInput_base: Schema.Class<UpdateTaskInput, Schema.Struct<{
91
+ readonly completed: Schema.optional<Schema.Boolean>;
92
+ readonly id: Schema.String;
93
+ readonly notes: Schema.optional<Schema.String>;
94
+ readonly text: Schema.optional<Schema.String>;
95
+ }>, {}>;
96
+ export declare class UpdateTaskInput extends UpdateTaskInput_base {
97
+ }
98
+ declare const CreateTagInput_base: Schema.Class<CreateTagInput, Schema.Struct<{
99
+ readonly name: Schema.String;
100
+ }>, {}>;
101
+ export declare class CreateTagInput extends CreateTagInput_base {
102
+ }
103
+ declare const UpdateChecklistItemInput_base: Schema.Class<UpdateChecklistItemInput, Schema.Struct<{
104
+ readonly completed: Schema.optional<Schema.Boolean>;
105
+ readonly itemId: Schema.String;
106
+ readonly taskId: Schema.String;
107
+ readonly text: Schema.optional<Schema.String>;
108
+ }>, {}>;
109
+ export declare class UpdateChecklistItemInput extends UpdateChecklistItemInput_base {
110
+ }
111
+ export {};
@@ -0,0 +1,94 @@
1
+ import { Schema } from "effect";
2
+ export const TaskType = Schema.Literals(["habit", "daily", "todo", "reward"]);
3
+ export const Direction = Schema.Literals(["up", "down"]);
4
+ class HabiticaStats extends Schema.Class("HabiticaStats")({
5
+ class: Schema.optional(Schema.String),
6
+ gp: Schema.Number,
7
+ hp: Schema.Number,
8
+ lvl: Schema.Number,
9
+ mp: Schema.Number,
10
+ toNextLevel: Schema.optional(Schema.Number),
11
+ }) {
12
+ }
13
+ export class HabiticaProfile extends Schema.Class("HabiticaProfile")({
14
+ id: Schema.String,
15
+ displayName: Schema.String,
16
+ stats: HabiticaStats,
17
+ }) {
18
+ }
19
+ export class HabiticaChecklistItem extends Schema.Class("HabiticaChecklistItem")({
20
+ completed: Schema.Boolean,
21
+ id: Schema.String,
22
+ text: Schema.String,
23
+ }) {
24
+ }
25
+ export class HabiticaTask extends Schema.Class("HabiticaTask")({
26
+ checklist: Schema.optional(Schema.Array(HabiticaChecklistItem)),
27
+ completed: Schema.optional(Schema.Boolean),
28
+ id: Schema.String,
29
+ notes: Schema.optional(Schema.String),
30
+ text: Schema.String,
31
+ type: TaskType,
32
+ }) {
33
+ }
34
+ export class HabiticaTag extends Schema.Class("HabiticaTag")({
35
+ id: Schema.String,
36
+ name: Schema.String,
37
+ }) {
38
+ }
39
+ export class HabiticaNotification extends Schema.Class("HabiticaNotification")({
40
+ id: Schema.String,
41
+ seen: Schema.Boolean,
42
+ text: Schema.String,
43
+ type: Schema.String,
44
+ }) {
45
+ }
46
+ export class HabiticaInventory extends Schema.Class("HabiticaInventory")({
47
+ eggs: Schema.Record(Schema.String, Schema.Number),
48
+ food: Schema.Record(Schema.String, Schema.Number),
49
+ hatchingPotions: Schema.Record(Schema.String, Schema.Number),
50
+ mounts: Schema.Record(Schema.String, Schema.Boolean),
51
+ pets: Schema.Record(Schema.String, Schema.Number),
52
+ }) {
53
+ }
54
+ export class HabiticaShopItem extends Schema.Class("HabiticaShopItem")({
55
+ key: Schema.String,
56
+ text: Schema.String,
57
+ value: Schema.Number,
58
+ }) {
59
+ }
60
+ export class HabiticaSkill extends Schema.Class("HabiticaSkill")({
61
+ key: Schema.String,
62
+ mana: Schema.Number,
63
+ text: Schema.String,
64
+ }) {
65
+ }
66
+ export class HabiticaMutationResult extends Schema.Class("HabiticaMutationResult")({
67
+ id: Schema.String,
68
+ message: Schema.String,
69
+ }) {
70
+ }
71
+ export class CreateTaskInput extends Schema.Class("CreateTaskInput")({
72
+ notes: Schema.optional(Schema.String),
73
+ text: Schema.String,
74
+ type: TaskType,
75
+ }) {
76
+ }
77
+ export class UpdateTaskInput extends Schema.Class("UpdateTaskInput")({
78
+ completed: Schema.optional(Schema.Boolean),
79
+ id: Schema.String,
80
+ notes: Schema.optional(Schema.String),
81
+ text: Schema.optional(Schema.String),
82
+ }) {
83
+ }
84
+ export class CreateTagInput extends Schema.Class("CreateTagInput")({
85
+ name: Schema.String,
86
+ }) {
87
+ }
88
+ export class UpdateChecklistItemInput extends Schema.Class("UpdateChecklistItemInput")({
89
+ completed: Schema.optional(Schema.Boolean),
90
+ itemId: Schema.String,
91
+ taskId: Schema.String,
92
+ text: Schema.optional(Schema.String),
93
+ }) {
94
+ }
@@ -0,0 +1,15 @@
1
+ import { Context, type Effect } from "effect";
2
+ import type { HabiticaError } from "./HabiticaErrors.js";
3
+ export interface HabiticaTransportRequest {
4
+ readonly body?: unknown;
5
+ readonly method: "DELETE" | "GET" | "POST" | "PUT";
6
+ readonly path: string;
7
+ readonly urlParams?: Readonly<Record<string, string>>;
8
+ }
9
+ export interface HabiticaTransportShape {
10
+ readonly request: <A>(request: HabiticaTransportRequest, decode: (value: unknown) => Effect.Effect<A, HabiticaError>) => Effect.Effect<A, HabiticaError>;
11
+ }
12
+ declare const HabiticaTransport_base: Context.ServiceClass<HabiticaTransport, "habitica-mcp/HabiticaTransport", HabiticaTransportShape>;
13
+ export declare class HabiticaTransport extends HabiticaTransport_base {
14
+ }
15
+ export {};
@@ -0,0 +1,3 @@
1
+ import { Context } from "effect";
2
+ export class HabiticaTransport extends Context.Service()("habitica-mcp/HabiticaTransport") {
3
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./HabiticaMcp.js";
3
+ run();
@@ -0,0 +1,3 @@
1
+ export declare const DailyPlanningPrompt: import("effect/Layer").Layer<never, never, never>;
2
+ export declare const TaskReviewPrompt: import("effect/Layer").Layer<never, never, never>;
3
+ export declare const HabitCheckInPrompt: import("effect/Layer").Layer<never, never, never>;
@@ -0,0 +1,35 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { McpServer } from "effect/unstable/ai";
3
+ export const DailyPlanningPrompt = McpServer.prompt({
4
+ name: "Daily Planning",
5
+ description: "Plan a Habitica day from current tasks and stats.",
6
+ parameters: {
7
+ focus: Schema.optional(Schema.String),
8
+ },
9
+ completion: {
10
+ focus: () => Effect.succeed(["dailies", "todos", "habits", "rewards"]),
11
+ },
12
+ content: ({ focus }) => Effect.succeed(`Use GetStatsTool and ListTasksTool to plan today's Habitica work${focus === undefined ? "." : ` for ${focus}.`}`),
13
+ });
14
+ export const TaskReviewPrompt = McpServer.prompt({
15
+ name: "Task Review",
16
+ description: "Review Habitica tasks and suggest safe updates.",
17
+ parameters: {
18
+ taskType: Schema.optional(Schema.String),
19
+ },
20
+ completion: {
21
+ taskType: () => Effect.succeed(["habit", "daily", "todo", "reward"]),
22
+ },
23
+ content: ({ taskType }) => Effect.succeed(`Use ListTasksTool${taskType === undefined ? "" : ` filtered to ${taskType}`} and propose explicit changes before using mutating tools.`),
24
+ });
25
+ export const HabitCheckInPrompt = McpServer.prompt({
26
+ name: "Habit Check-In",
27
+ description: "Check in on Habitica habits without scoring them automatically.",
28
+ parameters: {
29
+ mood: Schema.optional(Schema.String),
30
+ },
31
+ completion: {
32
+ mood: () => Effect.succeed(["steady", "blocked", "low-energy", "high-energy"]),
33
+ },
34
+ content: ({ mood }) => Effect.succeed(`Use ListTasksTool for habits and ask before ScoreTaskTool${mood === undefined ? "." : `; user mood: ${mood}.`}`),
35
+ });
@@ -0,0 +1,2 @@
1
+ export declare const CapabilitiesResource: import("effect/Layer").Layer<never, never, never>;
2
+ export declare const TaskTemplateResource: import("effect/Layer").Layer<never, never, never>;
@@ -0,0 +1,25 @@
1
+ import { Effect } from "effect";
2
+ import { McpServer } from "effect/unstable/ai";
3
+ export const CapabilitiesResource = McpServer.resource({
4
+ uri: "habitica-mcp://capabilities",
5
+ name: "Habitica MCP Capabilities",
6
+ description: "Describes the supported Habitica read and write tool domains.",
7
+ mimeType: "text/markdown",
8
+ content: Effect.succeed(`# Habitica MCP Capabilities
9
+
10
+ This server exposes typed Habitica tools for profile, stats, tasks, tags, checklists, notifications,
11
+ inventory, rewards, shop items, pets, mounts, and skills.
12
+
13
+ Mutating tools use explicit verb names and request approval. Stdio stdout is reserved for MCP JSON-RPC.`),
14
+ });
15
+ export const TaskTemplateResource = McpServer.resource({
16
+ uri: "habitica-mcp://task-template",
17
+ name: "Habitica Task Template",
18
+ description: "Suggested fields for creating or updating Habitica tasks.",
19
+ mimeType: "application/json",
20
+ content: Effect.succeed(JSON.stringify({
21
+ notes: "Optional notes visible on the task.",
22
+ text: "Clear task text.",
23
+ type: "habit | daily | todo | reward",
24
+ }, null, 2)),
25
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,354 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Tool, Toolkit } from "effect/unstable/ai";
3
+ import { HabiticaErrorSchema } from "../habitica/HabiticaErrors.js";
4
+ import { HabiticaGateway } from "../habitica/HabiticaGateway.js";
5
+ import { CreateTagInput, CreateTaskInput, Direction, HabiticaInventory, HabiticaMutationResult, HabiticaNotification, HabiticaProfile, HabiticaShopItem, HabiticaSkill, HabiticaTag, HabiticaTask, TaskType, UpdateChecklistItemInput, UpdateTaskInput, } from "../habitica/HabiticaSchemas.js";
6
+ const TaskIdInput = Schema.Struct({ taskId: Schema.String });
7
+ const RewardIdInput = Schema.Struct({ rewardId: Schema.String });
8
+ const NotificationIdInput = Schema.Struct({ notificationId: Schema.String });
9
+ const ListTasksInput = Schema.Struct({ type: Schema.optional(TaskType) });
10
+ const ScoreTaskInput = Schema.Struct({ direction: Direction, taskId: Schema.String });
11
+ const AddChecklistItemInput = Schema.Struct({ taskId: Schema.String, text: Schema.String });
12
+ const DeleteChecklistItemInput = Schema.Struct({ itemId: Schema.String, taskId: Schema.String });
13
+ const PetFoodInput = Schema.Struct({ foodKey: Schema.String, petKey: Schema.String });
14
+ const HatchPetInput = Schema.Struct({ eggKey: Schema.String, hatchingPotionKey: Schema.String });
15
+ const PetInput = Schema.Struct({ petKey: Schema.String });
16
+ const MountInput = Schema.Struct({ mountKey: Schema.String });
17
+ const SkillInput = Schema.Struct({
18
+ skillKey: Schema.String,
19
+ targetId: Schema.optional(Schema.String),
20
+ });
21
+ const ShopItemInput = Schema.Struct({ key: Schema.String });
22
+ const HelloWorldInput = Schema.Struct({ name: Schema.optional(Schema.String) });
23
+ const HabiticaFailure = { failure: HabiticaErrorSchema };
24
+ const HelloWorldTool = Tool.make("HelloWorldTool", {
25
+ description: "Return a deterministic greeting for MCP smoke tests.",
26
+ parameters: HelloWorldInput,
27
+ success: Schema.String,
28
+ })
29
+ .annotate(Tool.Readonly, true)
30
+ .annotate(Tool.Destructive, false)
31
+ .annotate(Tool.OpenWorld, false);
32
+ const GetUserProfileTool = Tool.make("GetUserProfileTool", {
33
+ ...HabiticaFailure,
34
+ description: "Read the current Habitica user profile.",
35
+ success: HabiticaProfile,
36
+ })
37
+ .annotate(Tool.Readonly, true)
38
+ .annotate(Tool.Destructive, false)
39
+ .annotate(Tool.OpenWorld, true);
40
+ const GetStatsTool = Tool.make("GetStatsTool", {
41
+ ...HabiticaFailure,
42
+ description: "Read the current Habitica stat block.",
43
+ success: HabiticaProfile.fields.stats,
44
+ })
45
+ .annotate(Tool.Readonly, true)
46
+ .annotate(Tool.Destructive, false)
47
+ .annotate(Tool.OpenWorld, true);
48
+ const ListTasksTool = Tool.make("ListTasksTool", {
49
+ ...HabiticaFailure,
50
+ description: "Read Habitica tasks, optionally filtered by task type.",
51
+ parameters: ListTasksInput,
52
+ success: Schema.Array(HabiticaTask),
53
+ })
54
+ .annotate(Tool.Readonly, true)
55
+ .annotate(Tool.Destructive, false)
56
+ .annotate(Tool.OpenWorld, true);
57
+ const GetTaskTool = Tool.make("GetTaskTool", {
58
+ ...HabiticaFailure,
59
+ description: "Read a single Habitica task by id.",
60
+ parameters: TaskIdInput,
61
+ success: HabiticaTask,
62
+ })
63
+ .annotate(Tool.Readonly, true)
64
+ .annotate(Tool.Destructive, false)
65
+ .annotate(Tool.OpenWorld, true);
66
+ const ListTagsTool = Tool.make("ListTagsTool", {
67
+ ...HabiticaFailure,
68
+ description: "Read Habitica tags.",
69
+ success: Schema.Array(HabiticaTag),
70
+ })
71
+ .annotate(Tool.Readonly, true)
72
+ .annotate(Tool.Destructive, false)
73
+ .annotate(Tool.OpenWorld, true);
74
+ const GetInventoryTool = Tool.make("GetInventoryTool", {
75
+ ...HabiticaFailure,
76
+ description: "Read Habitica inventory state.",
77
+ success: HabiticaInventory,
78
+ })
79
+ .annotate(Tool.Readonly, true)
80
+ .annotate(Tool.Destructive, false)
81
+ .annotate(Tool.OpenWorld, true);
82
+ const ListNotificationsTool = Tool.make("ListNotificationsTool", {
83
+ ...HabiticaFailure,
84
+ description: "Read Habitica notifications.",
85
+ success: Schema.Array(HabiticaNotification),
86
+ })
87
+ .annotate(Tool.Readonly, true)
88
+ .annotate(Tool.Destructive, false)
89
+ .annotate(Tool.OpenWorld, true);
90
+ const CreateTaskTool = Tool.make("CreateTaskTool", {
91
+ ...HabiticaFailure,
92
+ description: "Create a Habitica task.",
93
+ parameters: CreateTaskInput,
94
+ success: HabiticaTask,
95
+ needsApproval: true,
96
+ })
97
+ .annotate(Tool.Readonly, false)
98
+ .annotate(Tool.Destructive, false)
99
+ .annotate(Tool.OpenWorld, true);
100
+ const UpdateTaskTool = Tool.make("UpdateTaskTool", {
101
+ ...HabiticaFailure,
102
+ description: "Update a Habitica task.",
103
+ parameters: UpdateTaskInput,
104
+ success: HabiticaTask,
105
+ needsApproval: true,
106
+ })
107
+ .annotate(Tool.Readonly, false)
108
+ .annotate(Tool.Destructive, false)
109
+ .annotate(Tool.OpenWorld, true);
110
+ const DeleteTaskTool = Tool.make("DeleteTaskTool", {
111
+ ...HabiticaFailure,
112
+ description: "Delete a Habitica task.",
113
+ parameters: TaskIdInput,
114
+ success: HabiticaMutationResult,
115
+ needsApproval: true,
116
+ })
117
+ .annotate(Tool.Readonly, false)
118
+ .annotate(Tool.Destructive, true)
119
+ .annotate(Tool.OpenWorld, true);
120
+ const ScoreTaskTool = Tool.make("ScoreTaskTool", {
121
+ ...HabiticaFailure,
122
+ description: "Score a Habitica task up or down.",
123
+ parameters: ScoreTaskInput,
124
+ success: HabiticaTask,
125
+ needsApproval: true,
126
+ })
127
+ .annotate(Tool.Readonly, false)
128
+ .annotate(Tool.Destructive, false)
129
+ .annotate(Tool.OpenWorld, true);
130
+ const CreateTagTool = Tool.make("CreateTagTool", {
131
+ ...HabiticaFailure,
132
+ description: "Create a Habitica tag.",
133
+ parameters: CreateTagInput,
134
+ success: HabiticaTag,
135
+ needsApproval: true,
136
+ })
137
+ .annotate(Tool.Readonly, false)
138
+ .annotate(Tool.Destructive, false)
139
+ .annotate(Tool.OpenWorld, true);
140
+ const AddChecklistItemTool = Tool.make("AddChecklistItemTool", {
141
+ ...HabiticaFailure,
142
+ description: "Create a checklist item on a Habitica task.",
143
+ parameters: AddChecklistItemInput,
144
+ success: HabiticaTask,
145
+ needsApproval: true,
146
+ })
147
+ .annotate(Tool.Readonly, false)
148
+ .annotate(Tool.Destructive, false)
149
+ .annotate(Tool.OpenWorld, true);
150
+ const UpdateChecklistItemTool = Tool.make("UpdateChecklistItemTool", {
151
+ ...HabiticaFailure,
152
+ description: "Update a Habitica task checklist item.",
153
+ parameters: UpdateChecklistItemInput,
154
+ success: HabiticaTask,
155
+ needsApproval: true,
156
+ })
157
+ .annotate(Tool.Readonly, false)
158
+ .annotate(Tool.Destructive, false)
159
+ .annotate(Tool.OpenWorld, true);
160
+ const DeleteChecklistItemTool = Tool.make("DeleteChecklistItemTool", {
161
+ ...HabiticaFailure,
162
+ description: "Delete a Habitica task checklist item.",
163
+ parameters: DeleteChecklistItemInput,
164
+ success: HabiticaTask,
165
+ needsApproval: true,
166
+ })
167
+ .annotate(Tool.Readonly, false)
168
+ .annotate(Tool.Destructive, true)
169
+ .annotate(Tool.OpenWorld, true);
170
+ const ScoreChecklistItemTool = Tool.make("ScoreChecklistItemTool", {
171
+ ...HabiticaFailure,
172
+ description: "Score a Habitica task checklist item.",
173
+ parameters: DeleteChecklistItemInput,
174
+ success: HabiticaTask,
175
+ needsApproval: true,
176
+ })
177
+ .annotate(Tool.Readonly, false)
178
+ .annotate(Tool.Destructive, false)
179
+ .annotate(Tool.OpenWorld, true);
180
+ const ReadNotificationTool = Tool.make("ReadNotificationTool", {
181
+ ...HabiticaFailure,
182
+ description: "Mark a Habitica notification as read.",
183
+ parameters: NotificationIdInput,
184
+ success: HabiticaMutationResult,
185
+ needsApproval: true,
186
+ })
187
+ .annotate(Tool.Readonly, false)
188
+ .annotate(Tool.Destructive, false)
189
+ .annotate(Tool.OpenWorld, true);
190
+ const ListRewardsTool = Tool.make("ListRewardsTool", {
191
+ ...HabiticaFailure,
192
+ description: "Read Habitica reward tasks.",
193
+ success: Schema.Array(HabiticaTask),
194
+ })
195
+ .annotate(Tool.Readonly, true)
196
+ .annotate(Tool.Destructive, false)
197
+ .annotate(Tool.OpenWorld, true);
198
+ const CreateRewardTool = Tool.make("CreateRewardTool", {
199
+ ...HabiticaFailure,
200
+ description: "Create a Habitica reward.",
201
+ parameters: CreateTaskInput,
202
+ success: HabiticaTask,
203
+ needsApproval: true,
204
+ })
205
+ .annotate(Tool.Readonly, false)
206
+ .annotate(Tool.Destructive, false)
207
+ .annotate(Tool.OpenWorld, true);
208
+ const UpdateRewardTool = Tool.make("UpdateRewardTool", {
209
+ ...HabiticaFailure,
210
+ description: "Update a Habitica reward.",
211
+ parameters: UpdateTaskInput,
212
+ success: HabiticaTask,
213
+ needsApproval: true,
214
+ })
215
+ .annotate(Tool.Readonly, false)
216
+ .annotate(Tool.Destructive, false)
217
+ .annotate(Tool.OpenWorld, true);
218
+ const DeleteRewardTool = Tool.make("DeleteRewardTool", {
219
+ ...HabiticaFailure,
220
+ description: "Delete a Habitica reward.",
221
+ parameters: RewardIdInput,
222
+ success: HabiticaMutationResult,
223
+ needsApproval: true,
224
+ })
225
+ .annotate(Tool.Readonly, false)
226
+ .annotate(Tool.Destructive, true)
227
+ .annotate(Tool.OpenWorld, true);
228
+ const BuyRewardTool = Tool.make("BuyRewardTool", {
229
+ ...HabiticaFailure,
230
+ description: "Buy a Habitica reward.",
231
+ parameters: RewardIdInput,
232
+ success: HabiticaMutationResult,
233
+ needsApproval: true,
234
+ })
235
+ .annotate(Tool.Readonly, false)
236
+ .annotate(Tool.Destructive, false)
237
+ .annotate(Tool.OpenWorld, true);
238
+ const ListShopItemsTool = Tool.make("ListShopItemsTool", {
239
+ ...HabiticaFailure,
240
+ description: "Read Habitica shop items.",
241
+ success: Schema.Array(HabiticaShopItem),
242
+ })
243
+ .annotate(Tool.Readonly, true)
244
+ .annotate(Tool.Destructive, false)
245
+ .annotate(Tool.OpenWorld, true);
246
+ const BuyShopItemTool = Tool.make("BuyShopItemTool", {
247
+ ...HabiticaFailure,
248
+ description: "Buy a Habitica shop item.",
249
+ parameters: ShopItemInput,
250
+ success: HabiticaMutationResult,
251
+ needsApproval: true,
252
+ })
253
+ .annotate(Tool.Readonly, false)
254
+ .annotate(Tool.Destructive, false)
255
+ .annotate(Tool.OpenWorld, true);
256
+ const HatchPetTool = Tool.make("HatchPetTool", {
257
+ ...HabiticaFailure,
258
+ description: "Hatch a Habitica pet.",
259
+ parameters: HatchPetInput,
260
+ success: HabiticaMutationResult,
261
+ needsApproval: true,
262
+ })
263
+ .annotate(Tool.Readonly, false)
264
+ .annotate(Tool.Destructive, false)
265
+ .annotate(Tool.OpenWorld, true);
266
+ const FeedPetTool = Tool.make("FeedPetTool", {
267
+ ...HabiticaFailure,
268
+ description: "Feed a Habitica pet.",
269
+ parameters: PetFoodInput,
270
+ success: HabiticaMutationResult,
271
+ needsApproval: true,
272
+ })
273
+ .annotate(Tool.Readonly, false)
274
+ .annotate(Tool.Destructive, false)
275
+ .annotate(Tool.OpenWorld, true);
276
+ const EquipPetTool = Tool.make("EquipPetTool", {
277
+ ...HabiticaFailure,
278
+ description: "Equip a Habitica pet.",
279
+ parameters: PetInput,
280
+ success: HabiticaMutationResult,
281
+ needsApproval: true,
282
+ })
283
+ .annotate(Tool.Readonly, false)
284
+ .annotate(Tool.Destructive, false)
285
+ .annotate(Tool.OpenWorld, true);
286
+ const EquipMountTool = Tool.make("EquipMountTool", {
287
+ ...HabiticaFailure,
288
+ description: "Equip a Habitica mount.",
289
+ parameters: MountInput,
290
+ success: HabiticaMutationResult,
291
+ needsApproval: true,
292
+ })
293
+ .annotate(Tool.Readonly, false)
294
+ .annotate(Tool.Destructive, false)
295
+ .annotate(Tool.OpenWorld, true);
296
+ const ListSkillsTool = Tool.make("ListSkillsTool", {
297
+ ...HabiticaFailure,
298
+ description: "Read usable Habitica skills.",
299
+ success: Schema.Array(HabiticaSkill),
300
+ })
301
+ .annotate(Tool.Readonly, true)
302
+ .annotate(Tool.Destructive, false)
303
+ .annotate(Tool.OpenWorld, true);
304
+ const CastSkillTool = Tool.make("CastSkillTool", {
305
+ ...HabiticaFailure,
306
+ description: "Cast a Habitica skill.",
307
+ parameters: SkillInput,
308
+ success: HabiticaMutationResult,
309
+ needsApproval: true,
310
+ })
311
+ .annotate(Tool.Readonly, false)
312
+ .annotate(Tool.Destructive, false)
313
+ .annotate(Tool.OpenWorld, true);
314
+ /** @internal */
315
+ export const HabiticaToolkit = Toolkit.make(HelloWorldTool, GetUserProfileTool, GetStatsTool, ListTasksTool, GetTaskTool, ListTagsTool, GetInventoryTool, ListNotificationsTool, CreateTaskTool, UpdateTaskTool, DeleteTaskTool, ScoreTaskTool, CreateTagTool, AddChecklistItemTool, UpdateChecklistItemTool, DeleteChecklistItemTool, ScoreChecklistItemTool, ReadNotificationTool, ListRewardsTool, CreateRewardTool, UpdateRewardTool, DeleteRewardTool, BuyRewardTool, ListShopItemsTool, BuyShopItemTool, HatchPetTool, FeedPetTool, EquipPetTool, EquipMountTool, ListSkillsTool, CastSkillTool);
316
+ /** @internal */
317
+ export const HabiticaToolHandlers = Effect.gen(function* () {
318
+ const gateway = yield* HabiticaGateway;
319
+ return HabiticaToolkit.of({
320
+ AddChecklistItemTool: gateway.addChecklistItem,
321
+ BuyRewardTool: gateway.buyReward,
322
+ BuyShopItemTool: gateway.buyShopItem,
323
+ CastSkillTool: gateway.castSkill,
324
+ CreateRewardTool: gateway.createReward,
325
+ CreateTagTool: gateway.createTag,
326
+ CreateTaskTool: gateway.createTask,
327
+ DeleteChecklistItemTool: gateway.deleteChecklistItem,
328
+ DeleteRewardTool: gateway.deleteReward,
329
+ DeleteTaskTool: gateway.deleteTask,
330
+ EquipMountTool: gateway.equipMount,
331
+ EquipPetTool: gateway.equipPet,
332
+ FeedPetTool: gateway.feedPet,
333
+ GetInventoryTool: () => gateway.getInventory,
334
+ GetStatsTool: () => gateway.getStats,
335
+ GetTaskTool: gateway.getTask,
336
+ GetUserProfileTool: () => gateway.getUserProfile,
337
+ HelloWorldTool: ({ name }) => Effect.succeed(`Hello, ${name ?? "world"}!`),
338
+ HatchPetTool: gateway.hatchPet,
339
+ ListNotificationsTool: () => gateway.listNotifications,
340
+ ListRewardsTool: () => gateway.listTasks({ type: "reward" }),
341
+ ListShopItemsTool: () => gateway.listShopItems,
342
+ ListSkillsTool: () => gateway.listSkills,
343
+ ListTagsTool: () => gateway.listTags,
344
+ ListTasksTool: gateway.listTasks,
345
+ ReadNotificationTool: gateway.readNotification,
346
+ ScoreChecklistItemTool: gateway.scoreChecklistItem,
347
+ ScoreTaskTool: gateway.scoreTask,
348
+ UpdateChecklistItemTool: gateway.updateChecklistItem,
349
+ UpdateRewardTool: gateway.updateReward,
350
+ UpdateTaskTool: gateway.updateTask,
351
+ });
352
+ });
353
+ /** @internal */
354
+ export const HabiticaToolLayer = HabiticaToolkit.toLayer(HabiticaToolHandlers);
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "habitica-mcp",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "Habitica Model Context Protocol server built with Effect v4 beta",
5
+ "keywords": [
6
+ "effect",
7
+ "habitica",
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "typescript"
11
+ ],
12
+ "homepage": "https://github.com/tatemz/habitica-mcp#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/tatemz/habitica-mcp/issues"
15
+ },
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/tatemz/habitica-mcp.git"
20
+ },
21
+ "bin": {
22
+ "habitica-mcp": "dist/main.js"
23
+ },
24
+ "files": [
25
+ "dist/**/*.js",
26
+ "dist/**/*.d.ts",
27
+ "README.md"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": [],
31
+ "main": "./dist/HabiticaMcp.js",
32
+ "types": "./dist/HabiticaMcp.d.ts",
33
+ "exports": {
34
+ "./package.json": "./package.json",
35
+ ".": {
36
+ "types": "./dist/HabiticaMcp.d.ts",
37
+ "default": "./dist/HabiticaMcp.js"
38
+ }
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "provenance": true
43
+ },
44
+ "scripts": {
45
+ "build": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
46
+ "check": "pnpm build && pnpm typecheck && pnpm lint && pnpm format:check && pnpm test:coverage && pnpm e2e && pnpm mutation && pnpm lint:knip",
47
+ "dev": "tsx src/main.ts",
48
+ "e2e": "NODE_OPTIONS=\"--import tsx\" effect-bdd --features \"e2e/**/*.feature\" --steps \"e2e/**/*.step.ts\" --reporter text --strict",
49
+ "format": "oxfmt .",
50
+ "format:check": "oxfmt . --check",
51
+ "lint": "pnpm lint:policy && pnpm lint:rules && pnpm lint:versions && pnpm lint:quality-scope && pnpm lint:deps && pnpm lint:custom-rule-tests && pnpm lint:oxlint",
52
+ "lint:custom-rule-tests": "node scripts/lint-custom-rule-tests.mjs && node --test test/unit/oxlint-rules.test.mjs",
53
+ "lint:deps": "depcruise --config .dependency-cruiser.cjs src test e2e scripts oxlint-plugins",
54
+ "lint:knip": "knip --config knip.jsonc",
55
+ "lint:oxlint": "oxlint . --deny-warnings",
56
+ "lint:policy": "node scripts/lint-suppression-policy.mjs",
57
+ "lint:quality-scope": "node scripts/lint-quality-scope.mjs",
58
+ "lint:rules": "node scripts/lint-rules-policy.mjs",
59
+ "lint:versions": "node scripts/lint-version-policy.mjs",
60
+ "mutation": "stryker run",
61
+ "prepack": "pnpm build",
62
+ "prepare": "lefthook install",
63
+ "test": "vitest run",
64
+ "test:coverage": "vitest run --coverage",
65
+ "typecheck": "tsc -p tsconfig.test.json --noEmit"
66
+ },
67
+ "dependencies": {
68
+ "@effect/platform-node": "4.0.0-beta.78",
69
+ "effect": "4.0.0-beta.78"
70
+ },
71
+ "devDependencies": {
72
+ "@stryker-mutator/core": "^9.6.1",
73
+ "@stryker-mutator/vitest-runner": "^9.6.1",
74
+ "@types/node": "^25.9.2",
75
+ "@vitest/coverage-v8": "^4.1.9",
76
+ "dependency-cruiser": "^17.4.3",
77
+ "effect-bdd": "^0.4.0",
78
+ "knip": "^6.16.1",
79
+ "lefthook": "^2.1.9",
80
+ "oxfmt": "^0.54.0",
81
+ "oxlint": "^1.69.0",
82
+ "oxlint-tsgolint": "^0.23.0",
83
+ "tsx": "^4.22.3",
84
+ "typescript": "^6.0.3",
85
+ "vitest": "^4.1.8"
86
+ },
87
+ "engines": {
88
+ "node": ">=22.12.0"
89
+ },
90
+ "packageManager": "pnpm@10.16.1"
91
+ }