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 +146 -0
- package/dist/HabiticaMcp.d.ts +1 -0
- package/dist/HabiticaMcp.js +17 -0
- package/dist/config/HabiticaConfig.d.ts +13 -0
- package/dist/config/HabiticaConfig.js +16 -0
- package/dist/habitica/HabiticaErrors.d.ts +35 -0
- package/dist/habitica/HabiticaErrors.js +34 -0
- package/dist/habitica/HabiticaGateway.d.ts +77 -0
- package/dist/habitica/HabiticaGateway.js +3 -0
- package/dist/habitica/HabiticaHttpAdapter.d.ts +8 -0
- package/dist/habitica/HabiticaHttpAdapter.js +106 -0
- package/dist/habitica/HabiticaRoutes.d.ts +23 -0
- package/dist/habitica/HabiticaRoutes.js +22 -0
- package/dist/habitica/HabiticaSchemas.d.ts +111 -0
- package/dist/habitica/HabiticaSchemas.js +94 -0
- package/dist/habitica/HabiticaTransport.d.ts +15 -0
- package/dist/habitica/HabiticaTransport.js +3 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +3 -0
- package/dist/prompts/HabiticaPrompts.d.ts +3 -0
- package/dist/prompts/HabiticaPrompts.js +35 -0
- package/dist/resources/HabiticaResources.d.ts +2 -0
- package/dist/resources/HabiticaResources.js +25 -0
- package/dist/tools/HabiticaTools.d.ts +1 -0
- package/dist/tools/HabiticaTools.js +354 -0
- package/package.json +91 -0
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,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 {};
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -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,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
|
+
}
|